用C语言写内核
无论什么语言,要编译成 ELF文件格式(或者定一个其他的标准)
1 | int main() |
链接可以指定最终生成的可执行文件的起始虚拟地址,我们 指定 内核加载到 0x1500的地方,内核初始化的时候跳转内核要跳转到这个地方。
1 | ld kernel/main.o -Ttext Oxc0001500 -e main -o kernel/kernel.bin |
加载并执行 ELF kernrl
把编译完的内核代码加载到内存,分两步:
- 加载 ELF文件到内存
- 根据 ELF 文件格式 初始化 kernel ,链接的时候指定了 入口点,loader初始化内核后跳转就可以了
调试
我们把 MBR , loader的代码 放到 虚拟硬盘的前两个扇区, 同样的,我们把 编译完的 kernel 的二进制写到 磁盘的第9扇区后的200个扇区,不超过100k, loader里面 先拷贝 磁盘内容到 内存,初始化内核后,调到 内核入口点
1 | dd if=kernel.bin of=/your_path/hd60M.img bs=512 count=200 seek=9 conv=notrunc |
函数调用约定(以 cdecl 为例)
cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:
- 函数实参在线程栈上按照从右至左的顺序依次压栈。
- 函数结果保存在寄存器EAX/AX/AL中
- 浮点型结果存放在寄存器ST0中
- 编译后的函数名前缀以一个下划线字符
- 调用者负责从线程栈中弹出实参(即清栈)
- 8比特或者16比特长的整形实参提升为32比特长。
- 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
- 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
- RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)
系统调用
Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:
- 应用程序调用库函数(API);
- API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
中断处理函数返回到 API 中; - API 将 EAX 返回给应用程序。
应用程序调用系统调用的过程是:
- 把系统调用的编号存入 EAX;
- 把函数参数存入其它通用寄存器;
- 触发 0x80 号中断(int 0x80)
例子:
调用中断号,前提是这个中断函数已经写好了,系统已经提供,所以才称为 系统调用。1
2
3
4
5
6
7
8
9
10
11
12
13char* str=”hello,world\n”;
int count = 0;
void main() {
asm (”pusha; \
movl $4 ,%eax ; \
movl $1 , %ebx; \
movl str , %ecx;\
movl $12 ,%edx ; \
int $0x80; \
mov %eax,count;\
pop a \
”) ;
}