编译过程

前面我们都是使用汇编进行编程,从这节开始引入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 address0x8049020。这个地址是gcc编译程序后线性地址对应的入口地址。

hello

默认初始地址应该为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

测试