本文的封面图由OpenAIDALL·E模型生成。

MOS(项目仓库)是北航操作系统课程中用于教学的操作系统,运行在MIPS平台上,默认采用小端。

本文将在本地Linux平台(以Debian GNU/Linux 12 (bookworm) x86_64为例)上配置可完成该操作系统开发与调试的完整工具链,包含:

  • 交叉编译器的构建
  • 适用于本地开发的项目配置微调
  • 代码浏览体验优化(智能感知):VSCode + Clangd

交叉编译器

工具链平台

在通常,我们在本机上使用gcc等编译器,将C源代码翻译为适合本机执行的机器代码。

以常见的x86_64平台来说,这表明编译器本身在x86_64平台上运行,编译生成的二进制文件也在x86_64平台上运行。

但现实并非总是如此简单。有时,我们需要产生在其他平台上运行的二进制文件,但编译器并不能/不适宜在目标平台上运行(例如,目标平台的 CPU、存储资源有限)。事实上,编译器仅仅是一个翻译代码的工具,其翻译出的机器代码并不需要和编译器本身运行的平台相同。

事实上,对于一个编译工具链,其有三个相关的平台:

  • build:将编译工具链自身的源代码编译,产生编译工具链的平台;
  • host:编译工具链运行,将源代码翻译为机器代码的平台;
  • target:编译工具链生成的机器代码运行的平台。

build == host == target时,为「原生」(native)工具链;当build == host != target时,为「交叉」(cross)工具链。

三元组(Triplet)

那么,如何用简洁的方式描述buildhosttarget指的是什么平台呢?

可以使用如下三元组:

machine-vendor-operatingsystem

例如,对于x86_64 Linux平台,其三元组为:x86_64-pc-linux-gnu
对于MIPS32 little endian裸机,若目标文件类型符合ELF规范,其三元组为:mipsel-unknown-elf

可以通过gcc-dumpmachine选项取得当前编译器对应的target的三元组:

1
2
3
4
5
6
$ uname -a
Linux debian-vm 6.1.0-31-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.128-1 (2025-02-07) x86_64 GNU/Linux
$ gcc -dumpmachine
x86_64-linux-gnu
$ mips-unknown-elf-gcc -dumpmachine
mips-unknown-elf

在需要进行交叉编译时,在一个系统中可能存在多个目标平台不同的工具链,为了进行区分,通常在相关工具的名称前加上target三元组。

例如,对于目标平台为mips-unknown-elf的交叉工具链,其安装的文件如下:

1
2
3
4
5
6
7
8
9
$ mips-unknown-elf-
mips-unknown-elf-addr2line mips-unknown-elf-gcc-14.2.0 mips-unknown-elf-gdb-add-index mips-unknown-elf-ranlib
mips-unknown-elf-ar mips-unknown-elf-gcc-ar mips-unknown-elf-gstack mips-unknown-elf-readelf
mips-unknown-elf-as mips-unknown-elf-gcc-nm mips-unknown-elf-ld mips-unknown-elf-size
mips-unknown-elf-cc mips-unknown-elf-gcc-ranlib mips-unknown-elf-ld.bfd mips-unknown-elf-strings
mips-unknown-elf-c++filt mips-unknown-elf-gcov mips-unknown-elf-lto-dump mips-unknown-elf-strip
mips-unknown-elf-cpp mips-unknown-elf-gcov-dump mips-unknown-elf-nm
mips-unknown-elf-elfedit mips-unknown-elf-gcov-tool mips-unknown-elf-objcopy
mips-unknown-elf-gcc mips-unknown-elf-gdb mips-unknown-elf-objdump

MIPS与 mipsel

在交叉工具链以及QEMU等模拟器中,machine可能为mipsmipsel

例如:mips-unknown-elf-gccmipsel-unknown-elf-gccqemu-system-mipsqemu-system-mipsel

其中,mips代表大端mipsel代表小端MIPS Endian Little)。

不过事实上,无论是mipsel-unknown-elf-gcc还是mips-unknown-elf-gcc,都同时具有生成两种端序的代码的功能,仅仅是其默认端序不同。使用-EB参数将生成大端代码,使用-EL参数将生成小端代码。

通过 crosstool-ng 构建MIPS交叉工具链

要构建交叉工具链,传统的方法是下载gccbinutils等工具的源代码,手动配置buildhosttarget以及其他工具链参数后,编译产生交叉工具链。该过程需要复杂的手动操作,极易产生错误使得构建失败,或产生的交叉工具链不能正常工作。

crosstool-ng工具则自动化了上述过程,便于工具链的构建。

构建 crosstool-ng

虽然在部分发行版的仓库中有crosstool-ng,但仍建议手动从最新版本的源码构建,以免遇到奇怪的问题。

之后的步骤以Debian GNU/Linux 12 (bookworm) x86_64为例,其他发行版的操作应当大同小异。

首先,从官网下载最新的源代码包(在本文编写时为1.27.0)并解压。

1
2
3
wget http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-1.27.0.tar.bz2
tar -xf crosstool-ng-1.27.0.tar.bz2 crosstool-ng-1.27.0/
cd crosstool-ng-1.27.0

安装必要的依赖(或者,也可安装发行版提供的开发工具包组,例如Debian的build-essential,Arch Linux 的base-devel):

sudo apt install gcc make flex texinfo help2man patch gawk libtool libtool-bin bison libncurses-dev

运行配置脚本(构建生成的产物将存储在prefix参数指定的路径中):

./configure --prefix=/home/saite/build/crosstool-bin

配置脚本最终输出类似如下的内容,则表示配置成功;否则,需要检查脚本输出以找到失败的原因:

1
2
3
4
5
6
7
configure: creating ./config.status
config.status: creating Makefile
config.status: creating paths.sh
config.status: creating kconfig/Makefile
config.status: creating config/configure.in
config.status: creating config.h
config.status: executing depfiles commands

使用make进行构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ make
/usr/bin/gmake all-recursive
gmake[1]: Entering directory '/home/saite/build/crosstool-ng-1.27.0'
Making all in kconfig
gmake[2]: Entering directory '/home/saite/build/crosstool-ng-1.27.0/kconfig'
LEX lexer.lex.c
YACC parser.tab.c
/usr/bin/gmake all-am
gmake[3]: Entering directory '/home/saite/build/crosstool-ng-1.27.0/kconfig'
CC conf.o
CC confdata.o
CC expr.o
CC symbol.o
CC preprocess.o
CC util.o
CC parser.tab.o
CC lexer.lex.o
CCLD conf
CC nconf-nconf.o
CC nconf-nconf.gui.o
CC nconf-confdata.o
CC nconf-expr.o
CC nconf-symbol.o
CC nconf-preprocess.o
CC nconf-util.o
CC nconf-parser.tab.o
CC nconf-lexer.lex.o
CCLD nconf
CC mconf.o
CC lxdialog/checklist.o
CC lxdialog/inputbox.o
CC lxdialog/menubox.o
CC lxdialog/textbox.o
CC lxdialog/util.o
CC lxdialog/yesno.o
CCLD mconf/home/saite/build/cross-mips/src
gmake[3]: Leaving directory '/home/saite/build/crosstool-ng-1.27.0/kconfig'
gmake[2]: Leaving directory '/home/saite/build/crosstool-ng-1.27.0/kconfig'
gmake[2]: Entering directory '/home/saite/build/crosstool-ng-1.27.0'
GEN ct-ng
GEN bash-completion/ct-ng
GEN docs/ct-ng.1
gmake[2]: Leaving directory '/home/saite/build/crosstool-ng-1.27.0'
gmake[1]: Leaving directory '/home/saite/build/crosstool-ng-1.27.0'

使用make install将构建产物安装到之前指定的目录中,形成如下目录结构:

1
2
$ ls ~/build/crosstool-bin/
bin libexec share

为了便于访问,将${PREFIX}/bin添加到PATH中(也可将如下命令添加到~/.bashrc,以在所有bash会话中应用):

export PATH="${PATH}:/home/saite/build/crosstool-bin/bin"

添加完成后,执行ct-ng version,应当得到类似如下输出:

1
2
3
4
5
6
7
$ ct-ng version
This is crosstool-NG version 1.27.0

Copyright (C) 2008 Yann E. MORIN <yann.morin.1998@free.fr>
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

构建MIPS交叉工具链

为了便于配置,针对不同的交叉编译目标平台,crosstool-ng已经附带了许多经过测试的默认配置,可通过ct-ng list-samples查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ct-ng list-samples
Status Sample name
...
[G...] mips64el-multilib-linux-uclibc
[G...] mips64-unknown-linux-gnu
[G...] mips-ar2315-linux-gnu
[G...] mipsel-multilib-linux-gnu
[G...] mipsel-sde-elf
[G...] mipsel-unknown-linux-gnu
[G...] mips-malta-linux-gnu
[G...] mips-unknown-elf
[G...] mips-unknown-linux-gnu
[G...] mips-unknown-linux-uclibc
...
L (Local) : sample was found in current directory
G (Global) : sample was installed with crosstool-NG
X (EXPERIMENTAL): sample may use EXPERIMENTAL features
B (BROKEN) : sample is currently broken
O (OBSOLETE) : sample needs to be upgraded

其中mips-unknown-elf是用于生成在裸机平台上运行的符合ELF格式规范的可执行文件的,其默认端序是大端

查看MOS提供的构建脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ENDIAN is either EL (little endian) or EB (big endian)
ENDIAN := EL

ifeq ($(ENDIAN),EL)
QEMU := qemu-system-mipsel
else
QEMU := qemu-system-mips
endif
CROSS_COMPILE := mips-linux-gnu-
CC := $(CROSS_COMPILE)gcc
CFLAGS += --std=gnu99 -$(ENDIAN) -G 0 -mno-abicalls -fno-pic \
-ffreestanding -fno-stack-protector -fno-builtin \
-Wa,-xgot -Wall -mxgot -mno-fix-r4000 -march=4kc

注意其在CFLAGS中已经指定了端序-$(ENDIAN),故编译器的默认端序并不重要,上述mips-unknown-elf默认配置符合要求。

切换到任意工作目录中(例如,~/build/cross-mips),执行ct-ng mips-unknown-elf应用该默认配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ct-ng mips-unknown-elf
CONF mips-unknown-elf
#
# configuration written to .config
#

***********************************************************

Initially reported by: YEM
URL: http://ymorin.is-a-geek.org/

***********************************************************

Now configured for "mips-unknown-elf"

执行ct-ng menuconfig可在默认配置的基础上进行调整。

例如,在Paths and misc options中:

Local tarballs directory可指定构建过程中需要的源代码包的保存地址(注意该目录不会被自动创建,若该目录不存在,下载的源代码包将不会被保存):

构建路径配置

在本例中,使用了CT_TOP_DIR环境变量,表示执行ct-ng命令时的「当前目录」,更多这样的环境变量可在官方文档中查询。

Prefix directory指定了生成的交叉编译工具链的安装位置,默认为~/x-tools/<目标三元组>

Number of parallel jobs指定了可并行编译多少个文件,可根据CPU核心数以及内存容量设置(为了防止编译过程中内存耗尽,保守起见,建议确保每个并行任务可分配到 2GB 内存)。

并行编译数量配置

Target options中指定了交叉编译器目标平台的信息,一般不需要调整。

Toolchain options中指定了工具链配置,一般不需要调整。

Operating System指定了交叉编译生成的目标文件适用的操作系统,由于是用于操作系统开发,相当于在裸机上运行,保留默认的bare-metal即可。

Binary utilities是交叉编译binutils相关配置,保留默认即可。

C-library指定了编译器将使用的C标准库,由于是用于裸机开发,不含C标准库,保留默认的none即可。

C compiler是交叉编译gcc相关配置,保留默认即可。

Linkers是交叉编译链接器相关配置,保留默认即可。

Debug facilities用于配置要构建哪些用于「交叉调试」的工具(即,在本机上通过远程连接的方法调试另一目标平台上的程序),注意在crosstool-ng-1.27.0中,strace的构建似乎存在问题,不要勾选。

Companion librariesCompanion tools包含是否构建其他工具的配置,无需修改。

配置完成后退出,安装构建过程中需要的g++依赖:

sudo apt install g++

使用ct-ng build即可开始交叉工具链的构建,构建过程中会自动下载所需的源代码包,构建各种依赖,最终将交叉工具链安装到目标位置。

当出现类似如下信息时,表示构建成功:

1
2
3
4
5
6
7
8
9
10
[INFO ]  Finalizing the toolchain's directory
[INFO ] Stripping all toolchain executables
[EXTRA] Creating toolchain aliases
[EXTRA] Removing installed documentation
[EXTRA] Collect license information from: /home/saite/build/cross-mips/.build/mips-unknown-elf/src
[EXTRA] Put the license information to: /home/saite/x-tools/mips-unknown-elf/share/licenses
[INFO ] Finalizing the toolchain's directory: done in 1.16s (at 07:42)
[INFO ] Build completed at 20250309.115259
[INFO ] (elapsed: 7:41.91)
[INFO ] Finishing installation (may take a few seconds)...

为了方便使用,将交叉编译器路径加入PATH(根据实际情况调整路径):

export PATH="${PATH}:/home/saite/x-tools/mips-unknown-elf/bin"

为了后续开发中访问工具链,建议将上述命令加入~/.bashrc中。

添加完成后,执行mips-unknown-elf-gcc --version,应当得到类似如下输出:

1
2
3
4
5
$ mips-unknown-elf-gcc --version
mips-unknown-elf-gcc (crosstool-NG 1.27.0) 14.2.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

项目配置

clone项目:

1
2
3
4
5
6
7
8
$ git clone https://github.com/buaa-os/mos.public.git
Cloning into 'mos.public'...
remote: Enumerating objects: 271, done.
remote: Counting objects: 100% (271/271), done.
remote: Compressing objects: 100% (196/196), done.
remote: Total 271 (delta 48), reused 270 (delta 47), pack-reused 0 (from 0)
Receiving objects: 100% (271/271), 126.11 KiB | 285.00 KiB/s, done.
Resolving deltas: 100% (48/48), done.

以下以lab-solution子目录下的配置为例:cd lab-solution

安装QEMU模拟器:sudo apt install qemu-system-mips qemu-utils

安装调试所用的 gdb:sudo apt install gdb gdb-multiarch

注意在有的发行版中(例如 Arch Linux),gdb包已经包含了跨架构调试所需的内容,无需安装gdb-multiarch包。

本地环境配置文件微调

include.mk文件中,修改交叉工具链为之前构建好的工具链:

CROSS_COMPILE := mips-linux-gnu-改为CROSS_COMPILE := mips-unknown-elf-

若本地发行版gdb包已经包含了跨架构调试所需的内容,而无gdb-multiarch命令,还需修改Makefile,将dbg目标下的gdb-multiarch改为gdb

exec gdb -q $(mos_elf) -ex "target remote localhost:1234" $(dbg_elf)

本地开发与调试基本功能测试

配置完成后,使用make应当能成功编译项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ make
...
writing regular file '../user/init.b' into disk
writing regular file '../user/pingpong.b' into disk
writing regular file '../user/num.b' into disk
writing regular file '../user/sh.b' into disk
writing regular file 'rootfs/motd' into disk
writing regular file '../user/testbss.b' into disk
writing regular file '../user/testpipe.b' into disk
writing regular file 'rootfs/newmotd' into disk
writing regular file '../user/testptelibrary.b' into disk
writing regular file '../user/echo.b' into disk
writing regular file '../user/halt.b' into disk
writing regular file '../user/testarg.b' into disk
writing regular file '../user/testpiperace.b' into disk
writing regular file '../user/ls.b' into disk
writing regular file '../user/cat.b' into disk
writing regular file '../user/testfdsharing.b' into disk
make[2]: Leaving directory '/home/saite/code/mos.public/lab-solution/fs'
make[1]: Leaving directory '/home/saite/code/mos.public/lab-solution'

编译完成后生成的./target/mos文件即为MOS内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h target/mos
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: MIPSR3000
Version: 0x1
Entry point address: 0x800214e8
Start of program headers: 52 (bytes into file)
Start of section headers: 1027368 (bytes into file)
Flags: 0x50001001, noreorder, o32, mips32
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 23
Section header string table index: 22

使用make run,可在QEMU模拟器上运行内核,默认情况下,将得到输出:

init.c: mips_init() is called

1
2
3
$ make run
qemu-system-mipsel -cpu 4Kc -m 64 -nographic -M malta -drive id=ide0,file=target/fs.img,if=ide,format=raw -drive id=ide1,file=target/empty.img,if=ide,format=raw -no-reboot -kernel target/mos
init.c: mips_init() is called

使用make dbg,可在QEMU模拟器上运行内核,并使执行中断在机器复位后的第一条指令(0xBFC0 0000)处:

1
2
3
4
5
6
7
8
9
10
11
$ make dbg
export QEMU="qemu-system-mipsel"
export QEMU_FLAGS="-cpu 4Kc -m 64 -nographic -M malta -drive id=ide0,file=target/fs.img,if=ide,format=raw -drive id=ide1,file=target/empty.img,if=ide,format=raw -no-reboot"
export mos_elf="target/mos"
setsid ./tools/run_bg.sh $$ &
exec gdb-multiarch -q target/mos -ex "target remote localhost:1234"
trap: SIGINT: bad trap
Reading symbols from target/mos...
Remote debugging using localhost:1234
0xbfc00000 in ?? ()
(gdb)

智能感知配置

首先,确保clangd已经安装(sudo apt install clangd):

1
2
3
4
$ clangd --version
Debian clangd version 14.0.6
Features: linux+grpc
Platform: x86_64-pc-linux-gnu

在VSCode中安装clangd插件:

安装`clangd`插件

此时若直接打开MOS项目,由于缺乏编译选项配置文件,clangd无法确定项目结构,会出现找不到头文件的情况。

`clangd`无法确定项目结构

对于通过Makefile进行构建的项目,可以通过bear工具自动捕捉编译过程中的参数,生成clangd确定项目结构需要的compile_commands.json文件。

首先安装bear

sudo apt install bear

1
2
$ bear --version
bear 3.1.1

在MOS项目的lab-solution(或lab-exercise)目录下,执行:

1
bear -- make

这将生成compile_commands.json文件:

生成的`compile_commands.json`文件

但生成的compile_commands.json文件中含有clangd不支持的选项,直接使用仍然会出现错误。

不支持的选项导致错误

经测试,需要删除compile_commands.json中的-mno-fix-r4000-march=4kc选项:

1
2
sed -i '/-mno-fix-r4000/d' compile_commands.json
sed -i '/-march=4kc/d' compile_commands.json

之后重启clangd(可使用Ctrl+Shift+P打开命令面板,选择clangd: Restart language server

重启`clangd`

智能感知即可正常运行:

智能感知正常运行