引入c语言
编译过程
前面我们都是使用汇编进行编程,从这节开始引入c语言进行编写。我们知道C语言相对汇编语言,属于高级语言(^_^)。高级语言是需要编译成机器语言才可以执行的。为了让编译器编译得到的结果能够方便的在操作系统上执行,操作系统定义了二进制可执行文件的格式。linux上使用的就是ELF文件格式。
我们这次只是开发操作系统,没有做自己的C编译器,还是要使用gcc等编译器。所以我们的操作系统就要去兼容gcc编译得到的ELF文件结果。历史上,gcc也是在linux操作系统之前被开发出来的。
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
我们用hello world
走一遍这个流程。
我的环境是64位的,我们要编译32位的结果,所以中间会有一些特殊的编译选项。为了省事儿,也可以搭建32位的开发编译环境。
预处理
gcc -E hello.c -o hello.i
我们可以看到,7行的hello.c
源文件经过预处理后,得到了733行的hello.i
的文本文件。
编译器
gcc -S hello.i -o hello.s -m32
编译后得到了hello.s
的汇编代码。这里,-m32
是为了指定得到32位的结果,否则一些寄存器长度会错误。
汇编器
gcc -c hello.s -o hello.o -m32
经汇编之后,得到的是ELF 32-bit LSB relocatable
文件。
链接
ld -m elf_i386 hello.o -lc -e main
链接之后,得到的就是ELF 32-bit LSB executable
可执行文件。
- 因为我们使用标准库printf函数,所以需要链接标准库
-lc
- 程序入口是main函数,
-e main
用来指定入口
链接器ld的历史比gcc更古老。因为越早期,程序开发越接近二进制。伴随着自底向上的这些工具被发明,程序开发一步步从二进制->汇编->高级语言进行抽象简化。
ELF文件格式
ELF文件的具体细节就不展开了。使用readelf命令可以解析ELF文件,我们可以先看一下hello world
的解析结果。可以看到,Entry point address
为0x8049020
。这个地址是gcc编译程序后线性地址对应的入口地址。
默认初始地址应该为804800,但实测发现现在使用gcc编译的默认地址发生了变化
我们知道c语言的入口是main函数。实际上,gcc编译后ELF文件的默认入口是_start
,对应的地址是0x8049020
。这个地址可以通过修改连接器参数进行修改,我们将入口地址修改为0xc0001500
:
ld -m elf_i386 -Ttext 0xc0001500
这样,我们将编译得到的ELF文件,按照ELF文件的格式依次拷贝到对应的线性地址处,然后跳转到我们指定的地址0xc0001500
,就可以执行c语言写的代码了。
c调用汇编函数
gcc支持内联汇编,是可以在c语言中直接编写汇编指令的。但这个语法还是比较复杂的。我们使用最原始的方式。
如下是io_hlt
函数的汇编实现。
[bits 32]
section .text
global io_hlt
io_hlt:
hlt
ret
我们在c中声明io_hlt()
函数,并调用。
void io_hlt(void);
int main(void)
{
fin:
io_hlt();
goto fin;
}
在链接阶段,将汇编代码与c代码编译得到的可重定位文件(relocatable).o链接在一起,得到最终结果。
nasm -f elf -o func.o func.asm
gcc -m32 -c -o main.o main.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin main.o func.o
测试