基于qemu-riscv从0开始构建嵌入式linux系统ch22. 编译器与C/C++标准库

更换编译器

项目进行到后期我才发现,我们之前下载的二进制的编译器虽然方便使用,但是很多适合debug问题难免需要去编译器代的libc库里阅读具体实现,鉴于我们是学习性质的项目,干脆我们直接下载gcc的源码自己编译编译器。我们的目标是使用glibc的编译器要带有gdb功能方便调试应用,裸机项目使用代newlib的工具,便于在裸机开发时使用一些c库函数而不必自己实现。这次主要是使用了 https://github.com/riscv/riscv-gnu-toolchain tag:2021.08.07( gitee镜像:https://gitee.com/mirrors/riscv-gnu-toolchain ),仓库分别编译了用于linux的编译器和用于裸机开发的编译器,这样有三个好处,第一是裸机开发能使用newlib的c库,第二是不依赖别人提供的工具链,方便我们debug时跟踪源码,第三是我们编译了gdb工具无论是裸机还是linux上的app应用调试可以使用gdb分析问题。更新了编译器后重新编译target_root_app时我更新了一些编译脚本,优化了之前编译时忽略的细节。

# 用于裸机带newlib的gcc
./configure --prefix=/opt/gcc-riscv64-unknown-elf --with-cmodel=medany
make -j16
# 用于linux带glibc的gcc
./configure --prefix=/opt/gcc-riscv64-unknown-linux-gnu
make linux -j16

–with-cmodel=medany选项是需要注意的,我们的trusted_domain裸机程序会运行在高地址,因此务必配置该选项,现在可以使用newlib了,因此修改了一下启动脚本并做了相关newlib桩函数的移植。

另外在更换gcc-riscv64-unknown-linux-gnu后编译screen出现了无法打开的情况,我这里引用当时debug解决改问题的一段开发笔记。

debug screen工作不正常的问题,首先是执行screen后出现“[screen is terminating]”后就瞬间退出了,很莫名其妙,第一想法就是打开编译screen的Makefile文件,把“OPTIONS= -DDEBUG”打开,然后再次执行,会在/tmp/debug/{SCREEN,screen}.*产生log信息,查找log并对比了之前正常工作log,完全没发现什么异常,然后我尝试交叉编译了strace工具,没有什么异常发现,没有办法,只好去交叉编译了gdb工具(交叉编译gdb时会报出很多错误,主要还是官方的编译脚本有漏洞,均是路径错误,我手动修改后最终编译成功)生成了一个可以在目标板运行 的gdb工具,拷贝到目标板中,使用gdb调试一点点定位问题,再次打开编译screen的Makefile文件,修改成“CFLAGS = -g -O0 -D_GNU_SOURCE”方便我们使用gdb调试。很快我就发现screen.c:1235这里会调用fork,并且主进程在之后并没有做产生异常就收到了SIGHUP信号退出了,因此出现异常的应该是这里创建的子进程。我想起来之前strace可能没有检查子进程的log,于是重新使用“strace -f /bin/screen”检查,却还是没发现什么异常,奇怪!只好继续用gdb跟踪子进程,方法是进入gdb后,使用“set follow-fork-mode child”命令来指示gdb跟踪子进程,经过一番定位最终screen.c:1428的MakeWindow(&nwin)返回了-1引起错误,进一步跟踪发现产生错误的调用关系是这样:

screen.c:1428:MakeWindow(&nwin)
>>>>window.c:628:OpenDevice(nwin.args, nwin.lflag, &type, &TtyName)
>>>>>>>>window.c:1132:OpenPTY(namep)
>>>>>>>>>>>>pty.c:334:openpty(&f, &s, TtyName, NULL, NULL)

看来问题就出在openpty,这个函数定义在glibc中login/openpty.c:86行,使用gdb深入查找我们发现openpty.c:99:master = getpt ();返回了-1,而getpt的定义有三个分别在login/getpt.c和sysdeps/unix/bsd/getpt.c和sysdeps/unix/sysv/linux/getpt.c,我大概猜到问题了,应该是我们自己编译的编译器和下载bootlin上的编译器这样使用了不同的getpt,经过确认我们自己编译器使用的是sysdeps/unix/sysv/linux/getpt.c,而bootlin上的编译器我只能猜测使用的是sysdeps/unix/bsd/getpt.c,那么区别是什么呢?linux风格的pty设备是打开/dev/ptmx进而在/dev/pts目录内产生pty节点设备,而BSD风格的pty设备则是直接使用/dev/pty*设备。那么我自己编写了简单的测试代码open设备/dev/ptmx,发现果然返回-1,只好进一步查看内核的相关配置,发现其实内核里对两种风格的pty设备都支持了,如下:

CONFIG_UNIX98_PTYS=y
CONFIG_LEGACY_PTYS=y
CONFIG_LEGACY_PTY_COUNT=256

但是要注意到内核文档>devices.rst第155行提到了要将devpts挂载到/dev/pts这样linux风格的pty设备才能使用,原来是这样( ^_^ )!我们在目标文件系统配置文件/etc/fstab使用mdev的方式生成/dev目录,但是并没有给我们创建/dev/pts目录,因此不能直接在/etc/fstab添加挂载设备信息,那我们就还是在/etc/init.d/rcS启动脚本中添加,在/sbin/mdev -s后添加

mkdir /dev/pts
mount -t devpts devpts /dev/pts

OK,测试一下screen,完美运行,大功告成。

简谈libc

不知道大家在阅读上面的一个debug问题实例中有没有对libc库更加清晰一点呢,在libc库中有大量的平台相关代码,这里的平台既包括arch架构又保存操作系统(linux/windous)等。而如果你没有在对应平台上进行开发交叉编译产生的可执行文件就未必能正常运行,但是像我们之前的opensbi、uboot、kernel似乎都使用了glic的编译器,这是因为这些项目均无一例外的根本没有使用任何libc的函数,像是kernel中的很多str操作函数,print操作函数均为kernel自己实现的,而我们在开发trusted_domain代码时,作为裸机程序或rtos环境往往没有提供自己实现的str函数,而这时候也不适合使用glibc中的接口,因为阅读源码可以发现很多代码都有些平台相关的东西c++库更甚,因此才有常见的裸机嵌入式开发环境使用newlib来作为默认c库给用户使用,newlib需要用户实现自己的平台桩函数,就不会有更多的平台依赖了。

本教程的
github仓库:https://github.com/QQxiaoming/quard_star_tutorial
gitee仓库:https://gitee.com/QQxiaoming/quard_star_tutorial
本节所在tag:ch22