PS:所有的批注都写在了块引用中,其他文字均为题干
print函数是学习几乎任何一种软件开发语言时最先学习使用的函数,同时该函数也是最基本和原始的程序调试手段,但该函数的实现却并不简单。本实验的目的在于理解操作系统与硬件的接口方法,并实现一个可打印字符的函数(非系统调用),用于后续的调试和开发。
一、了解virt机器
操作系统介于硬件和应用程序之间,向下管理硬件资源,向上提供应用编程接口。设计并实现操作系统需要熟悉底层硬件的组成及其操作方法。
本系列实验都会在QEMU模拟器上完成,首先来了解一下模拟的机器信息。可以通过下列两种方法:
1.查看QEMU关于 virt的描述 , 或者查看QEMU的源码,如github上的 virt.h 和 virt.c。virt.c中可见如下有关内存映射的内容
static const MemMapEntry base_memmap[] = {
/* Space up to 0x8000000 is reserved for a boot ROM */
[VIRT_FLASH] = { 0, 0x08000000 },
[VIRT_CPUPERIPHS] = { 0x08000000, 0x00020000 },
/* GIC distributor and CPU interfaces sit inside the CPU peripheral space */
[VIRT_GIC_DIST] = { 0x08000000, 0x00010000 },
[VIRT_GIC_CPU] = { 0x08010000, 0x00010000 },
[VIRT_GIC_V2M] = { 0x08020000, 0x00001000 },
[VIRT_GIC_HYP] = { 0x08030000, 0x00010000 },
[VIRT_GIC_VCPU] = { 0x08040000, 0x00010000 },
/* The space in between here is reserved for GICv3 CPU/vCPU/HYP */
[VIRT_GIC_ITS] = { 0x08080000, 0x00020000 },
/* This redistributor space allows up to 2*64kB*123 CPUs */
[VIRT_GIC_REDIST] = { 0x080A0000, 0x00F60000 },
[VIRT_UART] = { 0x09000000, 0x00001000 },
[VIRT_RTC] = { 0x09010000, 0x00001000 },
[VIRT_FW_CFG] = { 0x09020000, 0x00000018 },
[VIRT_GPIO] = { 0x09030000, 0x00001000 },
[VIRT_SECURE_UART] = { 0x09040000, 0x00001000 },
[VIRT_SMMU] = { 0x09050000, 0x00020000 },
[VIRT_PCDIMM_ACPI] = { 0x09070000, MEMORY_HOTPLUG_IO_LEN },
[VIRT_ACPI_GED] = { 0x09080000, ACPI_GED_EVT_SEL_LEN },
[VIRT_NVDIMM_ACPI] = { 0x09090000, NVDIMM_ACPI_IO_LEN},
[VIRT_PVTIME] = { 0x090a0000, 0x00010000 },
[VIRT_SECURE_GPIO] = { 0x090b0000, 0x00001000 },
[VIRT_MMIO] = { 0x0a000000, 0x00000200 },
/* ...repeating for a total of NUM_VIRTIO_TRANSPORTS, each of that size */
[VIRT_PLATFORM_BUS] = { 0x0c000000, 0x02000000 },
[VIRT_SECURE_MEM] = { 0x0e000000, 0x01000000 },
[VIRT_PCIE_MMIO] = { 0x10000000, 0x2eff0000 },
[VIRT_PCIE_PIO] = { 0x3eff0000, 0x00010000 },
[VIRT_PCIE_ECAM] = { 0x3f000000, 0x01000000 },
/* Actual RAM size depends on initial RAM and device memory settings */
[VIRT_MEM] = { GiB, LEGACY_RAMLIMIT_BYTES },
};
2.通过QEMU导出设备树
①安装设备树格式转换工具
Mac下安装
brew install dtc
Linux下安装
apt-get install device-tree-compiler
在正常输入 apt-get install device-tree-compiler时,会出现下面问题:
这是因为apt-get命令一般需要root权限执行,所以最快捷的解决方法就是在apt-get前面加上sudo,即输入:
sudo apt-get install device-tree-compiler
然后就可以了
2.通过QEMU导出设备树并转成可读格式
qemu-system-aarch64 -machine virt,dumpdtb=virt.dtb -cpu cortex-a53 -nographic
dtc -I dtb -O dts -o virt.dts virt.dtb
输入完这两条指令后,路径下面出现virt.dtb和virt.dts这两个文件
virt.dtb转换后生成的virt.dts中可找到如下内容
pl011@9000000 {
clock-names = "uartclk\0apb_pclk";
clocks = <0x8000 0x8000>;
interrupts = <0x00 0x01 0x04>;
reg = <0x00 0x9000000 0x00 0x1000>;
compatible = "arm,pl011\0arm,primecell";
};
chosen {
stdout-path = "/pl011@9000000";
kaslr-seed = <0xcbd0568d 0xb463306c>;
};
由上可以看出,virt机器包含有pl011的设备,该设备的寄存器在0x9000000开始处。pl011实际上是一个UART设备,即串口。可以看到virt选择使用pl011作为标准输出,这是因为与PC不同,大部分嵌入式系统默认情况下并不包含VGA设备。
二、实现 PRT_Printf 函数
本系列实验每个实验均依赖前序相关实验,因此可拷贝 lab1 目录并重命名为 lab2 ,在 lab2 目录中再操作(后续实验照此操作)。
新建 src/bsp/print.c 文件,完成如下部分。
1.宏定义
在 print.c 中包含所需头文件,并定义后续将会用到的宏
#include <stdarg.h>
#include "prt_typedef.h"
#define UART_0_REG_BASE 0x09000000 // pl011 设备寄存器地址
// 寄存器及其位定义参见:https://developer.arm.com/documentation/ddi0183/g/programmers-model/summary-of-registers
#define DW_UART_THR 0x00 // UARTDR(Data Register) 寄存器
#define DW_UART_FR 0x18 // UARTFR(Flag Register) 寄存器
#define DW_UART_LCR_HR 0x2c // UARTLCR_H(Line Control Register) 寄存器
#define DW_XFIFO_NOT_FULL 0x020 // 发送缓冲区满置位
#define DW_FIFO_ENABLE 0x10 // 启用发送和接收FIFO
#define UART_BUSY_TIMEOUT 1000000
#define OS_MAX_SHOW_LEN 0x200
#define UART_REG_READ(addr) (*(volatile U32 *)(((uintptr_t)addr))) // 读设备寄存器
#define UART_REG_WRITE(value, addr) (*(volatile U32 *)((uintptr_t)addr) = (U32)value) // 写设备寄存器
强烈建议:做这个实验时候把lab1拷贝一遍,粘贴为lab2,然后在lab2基础上做实验二的任务。因为涉及到多处对于代码片段的修改,所以如果不拷贝的话,做完实验二再想做实验一还得再改回去
可以使用vim或者gedit创建print.c文件,代码拷贝进去粘贴
2.串口的初始化
U32 PRT_UartInit(void)
{
U32 result = 0;
U32 reg_base = UART_0_REG_BASE;
// LCR寄存器: https://developer.arm.com/documentation/ddi0183/g/programmers-model/register-descriptions/line-control-register--uartlcr-h?lang=en
result = UART_REG_READ((unsigned long)(reg_base + DW_UART_LCR_HR));
UART_REG_WRITE(result | DW_FIFO_ENABLE, (unsigned long)(reg_base + DW_UART_LCR_HR)); // 启用 FIFO
return OS_OK;
}
这里,把上述代码继续拷贝到print.c文件中,紧接着粘贴在下一行
3.往串口发送字符
// 读 reg_base + offset 寄存器的值。 uartno 参数未使用
S32 uart_reg_read(S32 uartno, U32 offset, U32 *val)
{
S32 ret;
U32 reg_base = UART_0_REG_BASE;
*val = UART_REG_READ((unsigned long)(reg_base + offset));
return OS_OK;
}
// 通过检查 FR 寄存器的标志位确定发送缓冲是否满,满时返回1.
S32 uart_is_txfifo_full(S32 uartno)
{
S32 ret;
U32 usr = 0;
ret = uart_reg_read(uartno, DW_UART_FR, &usr);
if (ret) {
return OS_OK;
}
return (usr & DW_XFIFO_NOT_FULL);
}
// 往 reg_base + offset 寄存器中写入值 val。
void uart_reg_write(S32 uartno, U32 offset, U32 val)
{
S32 ret;
U32 reg_base = UART_0_REG_BASE;
UART_REG_WRITE(val, (unsigned long)(reg_base + offset));
return;
}
// 通过轮询的方式发送字符到串口
void uart_poll_send(unsigned char ch)
{
S32 timeout = 0;
S32 max_timeout = UART_BUSY_TIMEOUT;
// 轮询发送缓冲区是否满
int result = uart_is_txfifo_full(0);
while (result) {
timeout++;
if (timeout >= max_timeout) {
return;
}
result = uart_is_txfifo_full(0);
}
// 如果缓冲区没满,通过往数据寄存器写入数据发送字符到串口
uart_reg_write(0, DW_UART_THR, (U32)(U8)ch);
return;
}
// 轮询的方式发送字符到串口,且转义换行符
void TryPutc(unsigned char ch)
{
uart_poll_send(ch);
if (ch == '\n') {
uart_poll_send('\r');
}
}
上面的代码很简单,就是通过轮询的方式向 PL011 的数据寄存器 DR 写入数据即可实现往串口发送字符,实现字符输出。
这里继续粘贴在print.c文件的尾部
4.支持格式化输出
extern int vsnprintf_s(char *buff, int buff_size, int count, char const *fmt, va_list arg);
int TryPrintf(const char *format, va_list vaList)
{
int len;
char buff[OS_MAX_SHOW_LEN] = {0};
char *str = buff;
len = vsnprintf_s(buff, OS_MAX_SHOW_LEN, OS_MAX_SHOW_LEN, format, vaList);
if (len == -1) {
return len;
}
while (*str != '\0') {
TryPutc(*str);
str++;
}
return OS_OK;
}
U32 PRT_Printf(const char *format, ...)
{
va_list vaList;
S32 count;
va_start(vaList, format);
count = TryPrintf(format, vaList);
va_end(vaList);
return count;
}
为了实现与 C 语言中 printf 函数类似的格式化功能,我们要用到可变参数列表 va_list 。而真正实现格式化控制转换的函数是 vsnprintf_s 函数。
这里又双叒叕继续粘贴在print.c文件的尾部
5.实现 vsnprintf_s 函数
新建 src/bsp/vsnprintf_s.c 实现 vsnprintf_s 函数
vsnprintf_s 函数的主要作用是依据格式控制符将可变参数列表转换成字符列表写入缓冲区。UniProton 提供了 libboundscheck 库,其中实现了 vsnprintf_s 函数,作为可选作业你可以试着使用 libboundscheck 库中的 vsnprintf_s 函数。简单起见,我们从另一个国产实时操作系统 RT-Thread 的 kservice.c 中引入了一个实现并进行了简单修改。 可以在 这里 下载 vsnprintf_s.c。
vsnprintf_s.c文件内代码如下:
#include <stdarg.h> static void ftoa_fixed(char *buffer, double value); static void ftoa_sci(char *buffer, double value); int strlen(const char *s) { const char *sc = (void *)0 ; for (sc = s; *sc != '\0'; ++sc) /* nothing */ ; return sc - s; } /* private function */ #define _ISDIGIT(c) ((unsigned)((c) - '0') < 10) /** * @brief This function will duplicate a string. * * @param n is the string to be duplicated. * * @param base is support divide instructions value. * * @return the duplicated string pointer. */ #ifdef RT_KPRINTF_USING_LONGLONG inline int divide(unsigned long long *n, int base) #else inline int divide(unsigned long *n, int base) #endif /* RT_KPRINTF_USING_LONGLONG */ { int res; /* optimized for processor which does not support divide instructions. */ #ifdef RT_KPRINTF_USING_LONGLONG res = (int)((*n) % base); *n = (long long)((*n) / base); #else res = (int)((*n) % base); *n = (long)((*n) / base); #endif return res; } int skip_atoi(const char **s) { int i = 0; while (_ISDIGIT(**s)) i = i * 10 + *((*s)++) - '0'; return i; } #define ZEROPAD (1 << 0) /* pad with zero */ #define SIGN (1 << 1) /* unsigned/signed long */ #define PLUS (1 << 2) /* show plus */ #define SPACE (1 << 3) /* space if plus */ #define LEFT (1 << 4) /* left justified */ #define SPECIAL (1 << 5) /* 0x */ #define LARGE (1 << 6) /* use 'ABCDEF' instead of 'abcdef' */ #define RT_PRINTF_PRECISION static char *print_number(char *buf, char *end, #ifdef RT_KPRINTF_USING_LONGLONG unsigned long long num, #else unsigned long num, #endif /* RT_KPRINTF_USING_LONGLONG */ int base, int qualifier, int s, #ifdef RT_PRINTF_PRECISION int precision, #endif /* RT_PRINTF_PRECISION */ int type) { char c = 0, sign = 0; #ifdef RT_KPRINTF_USING_LONGLONG char tmp[64] = {0}; #else char tmp[32] = {0}; #endif /* RT_KPRINTF_USING_LONGLONG */ int precision_bak = precision; const char *digits = (void *)0; static const char small_digits[] = "0123456789abcdef"; static const char large_digits[] = "0123456789ABCDEF"; int i = 0; int size = 0; size = s; digits = (type & LARGE) ? large_digits : small_digits; if (type & LEFT) { type &= ~ZEROPAD; } c = (type & ZEROPAD) ? '0' : ' '; /* get sign */ sign = 0; if (type & SIGN) { switch (qualifier) { case 'h': if ((short)num < 0) { sign = '-'; num = (unsigned short)-num; } break; case 'L': case 'l': if ((long)num < 0) { sign = '-'; num = (unsigned long)-num; } break; case 0: default: if ((int)num < 0) { sign = '-'; num = (unsigned int)-num; } break; } if (sign != '-') { if (type & PLUS) { sign = '+'; } else if (type & SPACE) { sign = ' '; } } } #ifdef RT_PRINTF_SPECIAL if (type & SPECIAL) { if (base == 2 || base == 16) { size -= 2; } else if (base == 8) { size--; } } #endif /* RT_PRINTF_SPECIAL */ i = 0; if (num == 0) { tmp[i++] = '0'; } else { while (num != 0) tmp[i++] = digits[divide(&num, base)]; } #ifdef RT_PRINTF_PRECISION if (i > precision) { precision = i; } size -= precision; #else size -= i; #endif /* RT_PRINTF_PRECISION */ if (!(type & (ZEROPAD | LEFT))) { if ((sign) && (size > 0)) { size--; } while (size-- > 0) { if (buf < end) { *buf = ' '; } ++ buf; } } if (sign) { if (buf < end) { *buf = sign; } -- size; ++ buf; } #ifdef RT_PRINTF_SPECIAL if (type & SPECIAL) { if (base == 2) { if (buf < end) *buf = '0'; ++ buf; if (buf < end) *buf = 'b'; ++ buf; } else if (base == 8) { if (buf < end) *buf = '0'; ++ buf; } else if (base == 16) { if (buf < end) { *buf = '0'; } ++ buf; if (buf < end) { *buf = type & LARGE ? 'X' : 'x'; } ++ buf; } } #endif /* RT_PRINTF_SPECIAL */ /* no align to the left */ if (!(type & LEFT)) { while (size-- > 0) { if (buf < end) { *buf = c; } ++ buf; } } #ifdef RT_PRINTF_PRECISION while (i < precision--) { if (buf < end) { *buf = '0'; } ++ buf; } #endif /* RT_PRINTF_PRECISION */ /* put number in the temporary buffer */ while (i-- > 0 && (precision_bak != 0)) { if (buf < end) { *buf = tmp[i]; } ++ buf; } while (size-- > 0) { if (buf < end) { *buf = ' '; } ++ buf; } return buf; } /** * @brief This function will fill a formatted string to buffer. * * @param buf is the buffer to save formatted string. * * @param size is the size of buffer. * * @param fmt is the format parameters. * * @param args is a list of variable parameters. * * @return The number of characters actually written to buffer. */ int vsnprintf_s(char *buf, int size, int no_count, const char *fmt, va_list args) { #ifdef RT_KPRINTF_USING_LONGLONG unsigned long long num = 0; #else unsigned long num = 0; #endif /* RT_KPRINTF_USING_LONGLONG */ int i = 0, len = 0; char *str = (void *)0, *end = (void *)0, c = 0; const char *s = (void *)0; unsigned char base = 0; /* the base of number */ unsigned char flags = 0; /* flags to print number */ unsigned char qualifier = 0; /* 'h', 'l', or 'L' for integer fields */ int field_width = 0; /* width of output field */ #ifdef RT_PRINTF_PRECISION int precision = 0; /* min. # of digits for integers and max for a string */ #endif /* RT_PRINTF_PRECISION */ str = buf; end = buf + size; /* Make sure end is always >= buf */ if (end < buf) { end = ((char *) - 1); size = end - buf; } for (; *fmt ; ++fmt) { if (*fmt != '%') { if (str < end) { *str = *fmt; } ++ str; continue; } /* process flags */ flags = 0; while (1) { /* skips the first '%' also */ ++ fmt; if (*fmt == '-') flags |= LEFT; else if (*fmt == '+') flags |= PLUS; else if (*fmt == ' ') flags |= SPACE; else if (*fmt == '#') flags |= SPECIAL; else if (*fmt == '0') flags |= ZEROPAD; else break; } /* get field width */ field_width = -1; if (_ISDIGIT(*fmt)) { field_width = skip_atoi(&fmt); } else if (*fmt == '*') { ++ fmt; /* it's the next argument */ field_width = va_arg(args, int); if (field_width < 0) { field_width = -field_width; flags |= LEFT; } } #ifdef RT_PRINTF_PRECISION /* get the precision */ precision = -1; if (*fmt == '.') { ++ fmt; if (_ISDIGIT(*fmt)) { precision = skip_atoi(&fmt); } else if (*fmt == '*') { ++ fmt; /* it's the next argument */ precision = va_arg(args, int); } if (precision < 0) { precision = 0; } } #endif /* RT_PRINTF_PRECISION */ /* get the conversion qualifier */ qualifier = 0; #ifdef RT_KPRINTF_USING_LONGLONG if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') #else if (*fmt == 'h' || *fmt == 'l') #endif /* RT_KPRINTF_USING_LONGLONG */ { qualifier = *fmt; ++ fmt; #ifdef RT_KPRINTF_USING_LONGLONG if (qualifier == 'l' && *fmt == 'l') { qualifier = 'L'; ++ fmt; } #endif /* RT_KPRINTF_USING_LONGLONG */ } /* the default base */ base = 10; switch (*fmt) { case 'c': if (!(flags & LEFT)) { while (--field_width > 0) { if (str < end) *str = ' '; ++ str; } } /* get character */ c = (unsigned char)va_arg(args, int); if (str < end) { *str = c; } ++ str; /* put width */ while (--field_width > 0) { if (str < end) *str = ' '; ++ str; } continue; case 's': s = va_arg(args, char *); if (!s) { s = "(NULL)"; } for (len = 0; (len != field_width) && (s[len] != '\0'); len++); #ifdef RT_PRINTF_PRECISION if (precision > 0 && len > precision) { len = precision; } #endif /* RT_PRINTF_PRECISION */ if (!(flags & LEFT)) { while (len < field_width--) { if (str < end) *str = ' '; ++ str; } } for (i = 0; i < len; ++i) { if (str < end) *str = *s; ++ str; ++ s; } while (len < field_width--) { if (str < end) *str = ' '; ++ str; } continue; case 'p': if (field_width == -1) { field_width = sizeof(void *) << 1; #ifdef RT_PRINTF_SPECIAL field_width += 2; /* `0x` prefix */ flags |= SPECIAL; #endif flags |= ZEROPAD; } #ifdef RT_PRINTF_PRECISION str = print_number(str, end, (unsigned long)va_arg(args, void *), 16, qualifier, field_width, precision, flags); #else str = print_number(str, end, (unsigned long)va_arg(args, void *), 16, qualifier, field_width, flags); #endif continue; case '%': if (str < end) { *str = '%'; } ++ str; continue; /* integer number formats - set up the flags and "break" */ case 'b': base = 2; break; case 'o': base = 8; break; case 'X': flags |= LARGE; case 'x': base = 16; break; case 'd': case 'i': flags |= SIGN; case 'u': break; default: if (str < end) { *str = '%'; } ++ str; if (*fmt) { if (str < end) { *str = *fmt; } ++ str; } else { -- fmt; } continue; } #ifdef RT_KPRINTF_USING_LONGLONG if (qualifier == 'L') { num = va_arg(args, unsigned long long); } else if (qualifier == 'l') #else if (qualifier == 'l') #endif /* RT_KPRINTF_USING_LONGLONG */ { num = va_arg(args, unsigned long); } else if (qualifier == 'h') { num = (unsigned short)va_arg(args, int); if (flags & SIGN) { num = (short)num; } } else { num = (unsigned int)va_arg(args, unsigned long); } #ifdef RT_PRINTF_PRECISION str = print_number(str, end, num, base, qualifier, field_width, precision, flags); #else str = print_number(str, end, num, base, qualifier, field_width, flags); #endif } if (size > 0) { if (str < end) { *str = '\0'; } else { end[-1] = '\0'; } } /* the trailing null byte doesn't count towards the total * ++str; */ return str - buf; }
6.调用 PRT_Printf 函数
main.c 修改为调用 PRT_Printf 函数输出信息。
#include "prt_typedef.h"
extern U32 PRT_Printf(const char *format, ...);
extern void PRT_UartInit(void);
S32 main(void)
{
PRT_UartInit();
PRT_Printf("Test PRT_Printf int format %d \n\n", 10);
}
main函数直接全部修改为上述代码。中间PRT_UartInit()和PRT_Printf("Test PRT_Printf int format %d \n\n", 10)之间留空隙是为了下一步输出字符
7.将新增文件纳入构建系统
修改 src/bsp/CMakeLists.txt 文件加入新增文件 print.c 和 vsnprintf_s.c
set(SRCS start.S prt_reset_vector.S print.c vsnprintf_s.c)
add_library(bsp OBJECT ${SRCS}) # OBJECT类型只编译生成.o目标文件,但不实际链接成库
和刚刚一样。这里也是把CMakeLists.txt全部修改为上述代码
8.启用 FPU
构建项目并执行发现程序没有任何输出。 需启用 FPU (src/bsp/start.S)。
Start:
LDR x1, =__os_sys_sp_end // ld文件中定义,堆栈设置
BIC sp, x1, #0xf
//参考: https://developer.arm.com/documentation/den0024/a/Memory-Ordering/Barriers/ISB-in-more-detail
Enable_FPU:
MRS X1, CPACR_EL1
ORR X1, X1, #(0x3 << 20)
MSR CPACR_EL1, X1
ISB
B OsEnterMain
注意:这里不是把start.s文件内内容全部替换,而是把上述代码插入到OsEl2Entry和OsEnterReset之间,保存。如下图所示:
9.Hello, miniEuler
再次构建项目并执行,发现已可正常输出。至此,我们获得了一个基本的输出和调试手段,如我们可以在系统崩溃时调用 PRT_Printf 函数进行输出。
我们可以利用 PRT_Printf 函数来打印一个文本 banner 让我们写的 OS 显得专业一点😁。 manytools.org 可以创建 ascii banner,选择你喜欢的样式和文字(下面选择的是 Standard 样式),然后在 main.c 的 main 函数中调用 PRT_Printf 输出。
S32 main(void)
{
PRT_UartInit();
PRT_Printf(" _ _ _____ _ _ _ _ _ _ _ _ \n");
PRT_Printf(" _ __ ___ (_)_ __ (_) ____| _| | ___ _ __ | |__ _ _ | | | | \\ | | | | | ___ _ __ \n");
PRT_Printf(" | '_ ` _ \\| | '_ \\| | _|| | | | |/ _ \\ '__| | '_ \\| | | | | |_| | \\| | | | |/ _ \\ '__|\n");
PRT_Printf(" | | | | | | | | | | | |__| |_| | | __/ | | |_) | |_| | | _ | |\\ | |_| | __/ | \n");
PRT_Printf(" |_| |_| |_|_|_| |_|_|_____\\__,_|_|\\___|_| |_.__/ \\__, | |_| |_|_| \\_|\\___/ \\___|_| \n");
PRT_Printf(" |___/ \n");
PRT_Printf("Test PRT_Printf int format %d \n\n", 10);
}
这里需要把PRT_Printf那7行输出插入到PRT_UartInit()和PRT_Printf("Test PRT_Printf int format %d \n\n", 10)之间,当然内容也可以根据自己爱好灵活修改
10.构建项目并执行
这一步的话,如果直接输入 sh runMiniEuler.sh 的话,会出现以下错误(其实也不算报错):
很巧合的是,它的输出和实验一的一样。原因就是,我们没有进行重新编译,运行的还是旧文件,这时候需要清空build文件夹,然后进入终端,在build目录下重新输入:
cmake ../src cmake --build .
进行编译 ,
编译完成后,重新输入
sh runMiniEuler.sh
运行新的程序,结果如下图所示:
到这一步就结束啦 ʕ•̼͛͡•ʕ-̺͛͡•ʔ•̮͛͡•ʔ ʕ•̫͡•ʕ*̫͡*ʕ•͓͡•ʔ-̫͡-ʕ•̫͡•ʔ*̫͡*ʔ-̫͡-ʔ