编译用户程序

别让简单问题复杂化.

第二章进行至此, 我们已经可以完整的构建整个 EOS, 并且同时生成可引导的软盘镜像了. 我们还为 Makefile 增加了一些便捷的新规则, 使得我们可以一键构建并启动 EOS.

不过, 我们目前的 OS lab 实验环境离“完全够用”还有一段距离. 在 EOS 的官方 IDE 中, 你不仅可以生成 EOS 内核本身, 还可以为 EOS 编写用户应用程序, 然后一同放入软盘镜像, 在启动 EOS 后执行. 所以我们至少还需要实现“编译用户应用程序”这一功能, 才能让我们的 OS lab 更加完善.

在这里, 教程作者想吐槽一下 EOS 官方的 IDE (虽然他基本没怎么用过这个软件): 如果你想写一个 EOS 用户应用程序, 你必须新建一个专门写用户程序的工程, 然后从内核工程中复制内核的 SDK, 再如此这般才能构建完成. 那假如你想同时修改你的用户应用程序和 EOS 内核, 你就必须在两个工程之间来回折腾, 场面一度异常混乱.

所以说, 不要让简单的问题复杂化, 接下来的教程将带你实现一个更为便利的构建用户程序的方法.

需要的文件

有了第二章的第一节的介绍, 你现在应该已经知道: 操作系统中用户应用程序的编译和执行, 依赖于编程语言为其提供的一系列基础设施, 以及运行时库. 好在我们无需自己手动实现, 直接从官方仓库获取即可.

将下载到的 crt 目录放入 eos/user 中:

eos
├── build
├── src
├── user
│   └── crt       # C Runtime
├── util
├── vm
├── Makefile
└── License.txt

除此之外, 用户应用程序还应当能调用 EOS 内核提供的 API, 我们在构建应用程序时需要确保 EOS 内核的 inc/eos.h, inc/eosdef.hinc/error.h 能被编译器找到. 在编写 Makefile 时, 我们需要把上述三个文件拷贝到 eos/user/sdk 中去; 如果这个目录不存在, Makefile 应当自动创建.

构建规则示例

.asm 构建为 .o

和构建内核时相同, 例如将 __alloca.asm 构建为 __alloca.o:

nasm -g __alloca.asm -o __alloca.bin -f win32

此处选择不输出 .lst, 因为没必要.

.c 构建为 .o

例如将 crt0.c 构建为 crt0.o:

i686-w64-mingw32-gcc -g crt0.c -o crt0.o                    \
                     -c -m32 -nostdlib -nostdinc            \
                     -fsigned-char -pipe -fno-builtin       \
                     -fno-omit-frame-pointer -ffreestanding \
                     -Isdk -Icrt/inc

其中 sdk 存放了三个 EOS 相关的头文件, crt/inc 存放了 C Runtime 的头文件.

.o 构建为 .exe

例如将 test.o 和所有的 Runtime 构建为 test.exe:

i686-w64-mingw32-ld test.o build/crt/*.o -o test.exe  \
                    -nostdlib -e __start              \
                    --major-subsystem-version 80      \
                    -Leos/build -lkernel

其中:

  • -e __start: C Runtime 中定义了入口函数 _start, C 源程序编译到目标文件时函数符号之前会加一个下划线, 于是这里的入口点需要指定为 __start;

  • --major-subsystem-version 80: EOS 内核在创建用户进程之前必须加载 PE 文件, 加载 PE 文件时会检查子系统版本, 不是 80 则会报错, 详情请 RTFSC;

  • -Leos/build: 指定链接器在此目录搜索链接库;

  • -lkernel: 指定静态链接导入库 libkernel.a. 一般所有的静态链接库都叫 libXXX.a, 所以静态链接时告诉链接器中间的 XXX 就行, 不用写全.

额外的规则

为了方便大家往软盘镜像中放入其他各种类型的文件 (例如最后一个实验会要求大家向软盘中添加 .txt 用于测试磁盘写入), 我们可以在 eos/user 中新建一个 import 目录. 每当执行 make 时, 自动将目录内的所有文件放入镜像.

编写 Makefile

eos/user 中新建 Makefile, 同时新建目录 src 存放所有需要编译的用户应用程序; 新建 import 存放其他需要导入的文件:

eos
├── build
├── src
├── user
│   ├── crt
│   ├── import    # 存放需要导入的文件, 暂为空
│   ├── src       # 存放应用程序的源文件, 暂为空
│   └── Makefile  # 构建脚本
├── util
├── vm
├── Makefile
└── License.txt

Makefile 内容如下:

# directories
USER_DIR = $(TOP_DIR)/user
SRC_DIR = $(USER_DIR)/src
CRT_DIR = $(USER_DIR)/crt
TARGET_DIR = $(USER_DIR)/build
OBJ_DIR = $(USER_DIR)/build/obj

# targets
CRT_TARGETS = $(patsubst $(CRT_DIR)/src/%.c, $(OBJ_DIR)/crt/%.o, $(wildcard $(CRT_DIR)/src/*.c))
CRT_TARGETS += $(patsubst $(CRT_DIR)/src/%.asm, $(OBJ_DIR)/crt/%.o, $(wildcard $(CRT_DIR)/src/*.asm))
USER_TARGETS = $(patsubst $(SRC_DIR)/%.c, $(TARGET_DIR)/%.exe, $(wildcard $(SRC_DIR)/*.c))

# flags
PRIVATE_CFLAGS := -I$(USER_DIR)/sdk -I$(CRT_DIR)/inc
PRIVATE_LDFLAGS := -L$(TOP_DIR)/build -e __start --major-subsystem-version 80

.PHONY: all clean crt app

all: app

crt: $(TARGET_DIR) $(OBJ_DIR) $(CRT_TARGETS)

app: crt $(USER_TARGETS)

clean:
	-rm -rf $(OBJ_DIR)/*
	-rm -f $(TARGET_DIR)/*.exe

$(TARGET_DIR):
	mkdir $(TARGET_DIR)

$(OBJ_DIR):
	mkdir $(OBJ_DIR)

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(PRIVATE_CFLAGS) -o $@ $^

$(TARGET_DIR)/%.exe: $(OBJ_DIR)/%.o $(CRT_TARGETS) $(TOP_DIR)/build/libkernel.a
	$(LD) $(PRIVATE_LDFLAGS) -o $@ $(CRT_TARGETS) $< -lkernel

$(OBJ_DIR)/crt/%.o: $(CRT_DIR)/src/%.c
	-mkdir -p $(dir $@)
	$(CC) $(PRIVATE_CFLAGS) -o $@ $^

$(OBJ_DIR)/crt/%.o: $(CRT_DIR)/src/%.asm
	-mkdir -p $(dir $@)
	$(NASM) -o $@ $<

此时我们需要更新 eos/Makefile:

--- Makefile
+++ Makefile
@@ -7,6 +7,7 @@
 OBJ_DIR = $(TOP_DIR)/build/obj
 LST_DIR = $(TOP_DIR)/build/lst
 KERNEL_SRC_DIR = $(TOP_DIR)/src
+USER_DIR = $(TOP_DIR)/user
 INCLUDE_ARG := -I$(KERNEL_SRC_DIR)/inc
 INCLUDE_ARG += -I$(KERNEL_SRC_DIR)/ke
 INCLUDE_ARG += -I$(KERNEL_SRC_DIR)/ob
@@ -22,24 +23,29 @@
 KERNEL_TARGETS += $(patsubst $(KERNEL_SRC_DIR)/%.asm, $(OBJ_DIR)/%.o, $(wildcard $(KERNEL_SRC_DIR)/**/**/*.asm))
 FLOPPY_FILES := $(TARGET_DIR)/loader.bin
 FLOPPY_FILES += $(TARGET_DIR)/kernel.dll
+USER_SDK_HEADERS := $(USER_DIR)/sdk/eos.h
+USER_SDK_HEADERS += $(USER_DIR)/sdk/eosdef.h
+USER_SDK_HEADERS += $(USER_DIR)/sdk/error.h
+USER_APP := $(patsubst $(USER_DIR)/src/%.c, $(USER_DIR)/build/%.exe, $(wildcard $(USER_DIR)/src/*.c))
+USER_IMPORT := $(wildcard $(USER_DIR)/import/*.*)

 # C compiler
 CFLAGS := $(DEBUG_ARG)
 CFLAGS += -m32 -c -nostdlib -nostdinc -fsigned-char -pipe
 CFLAGS += -fno-builtin -fno-omit-frame-pointer -ffreestanding
-CFLAGS += -D_KERNEL_ -D_I386
-CFLAGS += $(INCLUDE_ARG)
-CC = $(CROSS_PREFIX)gcc $(CFLAGS)
+PRIVATE_CFLAGS := -D_KERNEL_ -D_I386
+PRIVATE_CFLAGS += $(INCLUDE_ARG)
+export CC = $(CROSS_PREFIX)gcc $(CFLAGS)

 # assembler
 NASMFLAGS := $(DEBUG_ARG) -f win32
-NASM = nasm $(NASMFLAGS)
+export NASM = nasm $(NASMFLAGS)

 # linker
 LDFLAGS := -nostdlib
-LDFLAGS += -shared --image-base 0x80010000 -e _KiSystemStartup
-LDFLAGS += --out-implib $(TARGET_DIR)/libkernel.a
-LD = $(CROSS_PREFIX)ld $(LDFLAGS)
+PRIVATE_LDFLAGS := -shared --image-base 0x80010000 -e _KiSystemStartup
+PRIVATE_LDFLAGS += --out-implib $(TARGET_DIR)/libkernel.a
+export LD = $(CROSS_PREFIX)ld $(LDFLAGS)

 # virtual machine
 VMFLAGS := -q
@@ -53,12 +59,17 @@
 IMGEDIT_DIR = $(UTIL_DIR)/imgedit
 IMGEDIT = $(IMGEDIT_DIR)/build/imgedit

-.PHONY: all kernel run runvm clean
+.PHONY: all kernel user run runvm clean

-all: kernel $(TARGET_DIR)/floppy.img
+all: kernel user $(TARGET_DIR)/floppy.img

 kernel: $(TARGET_DIR) $(OBJ_DIR) $(LST_DIR) $(TARGET_DIR)/kernel.dll

+user: kernel
+	-mkdir $(USER_DIR)/sdk
+	$(MAKE) $(USER_SDK_HEADERS)
+	$(MAKE) -C $(USER_DIR)
+
 run: all
 	-$(VM) -f $(VMFILE)

@@ -66,6 +77,8 @@
 	-$(QEMU) file=$(TARGET_DIR)/floppy.img,if=floppy,format=raw

 clean:
+	$(MAKE) -C $(USER_DIR) clean
+	-rm -rf $(USER_DIR)/sdk/*
 	$(MAKE) -C $(IMGEDIT_DIR) clean
 	-rm -rf $(OBJ_DIR)/*
 	-rm -rf $(LST_DIR)/*
@@ -86,12 +99,12 @@

 # EOS kernel
 $(TARGET_DIR)/kernel.dll: $(KERNEL_TARGETS)
-	$(LD) -o $@ $(KERNEL_TARGETS)
+	$(LD) $(PRIVATE_LDFLAGS) -o $@ $(KERNEL_TARGETS)

 # EOS object
 $(OBJ_DIR)/%.o: $(KERNEL_SRC_DIR)/%.c
 	-mkdir -p $(dir $@)
-	$(CC) -o $@ $^
+	$(CC) $(PRIVATE_CFLAGS) -o $@ $^

 $(OBJ_DIR)/%.o: $(KERNEL_SRC_DIR)/%.asm
 	-mkdir -p $(dir $@)
@@ -104,6 +117,12 @@
 # floppy image
 $(TARGET_DIR)/floppy.img: $(TARGET_DIR)/boot.bin $(FLOPPY_FILES) $(IMGEDIT)
 	$(IMGEDIT) $@ -c $< $(FLOPPY_FILES)
+	-$(IMGEDIT) $@ -a $(USER_APP)
+	-$(IMGEDIT) $@ -a $(USER_IMPORT)
+
+# user SDK
+$(USER_DIR)/sdk/%.h: $(KERNEL_SRC_DIR)/inc/%.h
+	cp -f $< $@

 # imgedit
 $(IMGEDIT):

在 EOS 中运行 Hello World

到目前为止, 我们的 OS lab 基本上就搭建完成了. 现在我们可以来写一个 “Hello, world!” 来测试一下:

#include <stdio.h>

int main(int argc, const char *argv[]) {
  printf("Hello, world!\n");
  return 0;
}

将这个文件保存到 eos/user/src/hello.c, 然后重新构建 EOS:

cd ~/eos
make clean run

在虚拟机中我们输入 hello 后回车, 就可以看到 “Hello, world!” 已经出现在了控制台当中:

你好啊, 世界

当然, EOS 中还有一些其他命令, 比如 pt 可以显示进程/线程列表:

执行 “pt” 命令

奇怪的事情

好奇的小铭同学也想用自己刚搭建好的环境试试身手. 他首先输入了 hello, 但是一不小心输错打成了 helo, EOS 提示创建进程失败 (因为软盘镜像里根本没这个文件).

小铭没有理会这个问题, 他随后输入了 pt, 想看看系统里目前有什么进程. 结果敲下回车之后:

内核又又又双叒叕崩溃了!!!

完了, 自己是不是把 EOS 给玩坏了? 小铭同学喜欢刨根问底, 他后来又重新启动虚拟机, 多试了几次, 发现: 只要用户尝试启动一个不存在的程序之后, 再次执行 pt 命令, EOS 就一定会蓝屏.

恭喜, 你发现了一个内核 bug

并不是 EOS 被玩坏了, 而是 EOS 本身实现就有问题, 正常的操作系统显然不会出现这种低级错误. 你能尝试找出出现这个问题的原因吗?

这是 EOS 中比较明显的一个 bug, 但是它却比较难定位. 教程作者当时大概花了一个多小时, 才找出了导致这个 bug 的原因. 现在的你暂时没有办法解决这个问题, 因为我们还没有给 OS lab 增加调试功能. 但是在下一节之后, 你就可以尝试自行修复这个 bug 了.

Last updated

Was this helpful?