第二章进行至此, 我们已经可以完整的构建整个 EOS, 并且同时生成可引导的软盘镜像了. 我们还为 Makefile 增加了一些便捷的新规则, 使得我们可以一键构建并启动 EOS.
不过, 我们目前的 OS lab 实验环境离“完全够用”还有一段距离. 在 EOS 的官方 IDE 中, 你不仅可以生成 EOS 内核本身, 还可以为 EOS 编写用户应用程序, 然后一同放入软盘镜像, 在启动 EOS 后执行. 所以我们至少还需要实现“编译用户应用程序”这一功能, 才能让我们的 OS lab 更加完善.
在这里, 教程作者想吐槽一下 EOS 官方的 IDE (虽然他基本没怎么用过这个软件): 如果你想写一个 EOS 用户应用程序, 你必须新建一个专门写用户程序的工程, 然后从内核工程中复制内核的 SDK, 再如此这般才能构建完成. 那假如你想同时修改你的用户应用程序和 EOS 内核, 你就必须在两个工程之间来回折腾, 场面一度异常混乱.
所以说, 不要让简单的问题复杂化, 接下来的教程将带你实现一个更为便利的构建用户程序的方法.
需要的文件
有了第二章的第一节的介绍, 你现在应该已经知道: 操作系统中用户应用程序的编译和执行, 依赖于编程语言为其提供的一系列基础设施, 以及运行时库. 好在我们无需自己手动实现, 直接从 获取即可.
将下载到的 crt
目录放入 eos/user
中:
Copy eos
├── build
├── src
├── user
│ └── crt # C Runtime
├── util
├── vm
├── Makefile
└── License.txt
除此之外, 用户应用程序还应当能调用 EOS 内核提供的 API, 我们在构建应用程序时需要确保 EOS 内核的 inc/eos.h
, inc/eosdef.h
和 inc/error.h
能被编译器找到. 在编写 Makefile 时, 我们需要把上述三个文件拷贝到 eos/user/sdk
中去; 如果这个目录不存在, Makefile 应当自动创建.
构建规则示例
将 .asm
构建为 .o
和构建内核时相同, 例如将 __alloca.asm
构建为 __alloca.o
:
Copy nasm -g __alloca.asm -o __alloca.bin -f win32
此处选择不输出 .lst
, 因为没必要.
将 .c
构建为 .o
例如将 crt0.c
构建为 crt0.o
:
Copy 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
:
Copy 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
存放其他需要导入的文件:
Copy eos
├── build
├── src
├── user
│ ├── crt
│ ├── import # 存放需要导入的文件, 暂为空
│ ├── src # 存放应用程序的源文件, 暂为空
│ └── Makefile # 构建脚本
├── util
├── vm
├── Makefile
└── License.txt
Makefile 内容如下:
Copy # 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
:
Copy --- 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!” 来测试一下:
Copy #include <stdio.h>
int main(int argc, const char *argv[]) {
printf("Hello, world!\n");
return 0;
}
将这个文件保存到 eos/user/src/hello.c
, 然后重新构建 EOS:
Copy cd ~/eos
make clean run
在虚拟机中我们输入 hello
后回车, 就可以看到 “Hello, world!” 已经出现在了控制台当中:
当然, EOS 中还有一些其他命令, 比如 pt
可以显示进程/线程列表:
奇怪的事情
好奇的小铭同学也想用自己刚搭建好的环境试试身手. 他首先输入了 hello
, 但是一不小心输错打成了 helo
, EOS 提示创建进程失败 (因为软盘镜像里根本没这个文件).
小铭没有理会这个问题, 他随后输入了 pt
, 想看看系统里目前有什么进程. 结果敲下回车之后:
完了, 自己是不是把 EOS 给玩坏了? 小铭同学喜欢刨根问底, 他后来又重新启动虚拟机, 多试了几次, 发现: 只要用户尝试启动一个不存在的程序之后, 再次执行 pt
命令, EOS 就一定会蓝屏.
为什么说这是低级错误?
因为只要你尝试在 EOS 的控制台中输入几条指令, 你就有很大几率会触发这个 bug. 但是发生概率这么大的一件事, EOS 的开发者们却没有发现! (或者是发现了但是不去/无法修复?)
如果真的是开发者没有发现这个问题, 那就说明 EOS 在发布之前没有经过严谨的测试, 或者干脆就没测试. 在软件工程领域, 软件测试是一种有效且必要的质量保证手段, 科学合理的软件测试会给软件的质量带来飞跃般的提升.
所以, 如果你目前还对软件测试知之甚少, 并且没有在实际的软件开发中应用过任何测试手段, 你现在可以开始着手学习了.