写一个操作系统_14 C 链接与系统调用

用C语言写内核

无论什么语言,要编译成 ELF文件格式(或者定一个其他的标准)

1
2
3
4
5
int main()
{
while(1){};
return 0;
}

链接可以指定最终生成的可执行文件的起始虚拟地址,我们 指定 内核加载到 0x1500的地方,内核初始化的时候跳转内核要跳转到这个地方。

1
2
3
4
5
6
ld kernel/main.o -Ttext Oxc0001500 -e main -o kernel/kernel.bin

$$ ~>ld -help entry
Usage: ld [options] file...
Options:
-e ADDRESS, --entry ADDRESS Set start address

加载并执行 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
13
char* 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 \
”) ;
}

-->