现在,我们已经成功的进入了我们的内核。在之后更长的一段时间,我们可以使用更加高级的编程语言,也就是我们的C语言,而不是汇编来完成我们的工作。
C语言的确被广泛认为是一门非常接近硬件的编程语言,但是,对于一些比如我们后面将会做的——响应我们自己注册的中断和异常处理,底层的打印工作,进程切换等工作,仍然还是需要我们使用汇编完成。因此,使用汇编和C的联合编程成为我们之后的一个主要的工作。
从函数调用谈起
毫无疑问,C的最经典的写法就是使用面对过程的编程范式完成我们的工作。常见的过程抽象自然也就是我们的函数了。我们在之前就看到了——在我们汇编语言的世界里,存在各式各样的函数调用方式,比如说直接传递进入寄存器,比如说直接存放在栈中,比如说两者协调工作。参数的压入也是我们自己随意指定的,只需要按照栈的压入弹出方式就可以正确的恢复上下文的寄存器环境。
但是C语言把这些抽象加以规范后就屏蔽了,这就需要我们在编写操作系统这类底层软件的时候,还需要再次的进行C语言对参数调用传递的解析
int sub(int a, int b){
return a - b;
}
int main(){
int t = sub(3, 2);
}
上面的代码就是一个非常简单的C语言调用函数的例子。笔者推介一个好的网站:
Compiler Explorer,可以实时查看汇编
sub:
push ebp
mov ebp, esp
mov eax, DWORD PTR [ebp+8] ; 这里越过返回地址,拿到参数
sub eax, DWORD PTR [ebp+12]
pop ebp
ret
main:
push ebp
mov ebp, esp ; 这里是在缓存我们的main堆栈栈顶
sub esp, 16 ; 空出来位置
push 2 ; 依次压入2, 3的参数
push 3 ; 可以看到C默认的调用是从右向左入参
call sub
add esp, 8
mov DWORD PTR [ebp-4], eax
mov eax, 0
leave
ret
上面的代码需要按照-m32的方式进行编译,默认的64位会采取激进的寄存器传递的方式直接完成计算。
上面的整个调用约定就是经典的传统C调用约定,也就是cdecl(C Declarations)。下面的整个表就表达了我们的常见的参数调用约定。笔者这里枚举一下:
常见调用约定
找清理负责任人 | 类别 | 描述 |
---|---|---|
调用者清理 | cdecl | cdecl(C declaration,即C声明),起源于C语言的一种调用约定,在C语言中,函数参数从右到左的顺序入栈。GNU/Linux GCC,把这一约定作为事实上的标准,x86架构上的许多C编译器也都使用这个约定。在cdecl中,参数是在栈中传递的。EAX、ECX和EDX寄存器是由调用者保存的,其余寄存器由被调用者保存。函数的返回值存储在EAX寄存器。由调用者清理栈空间。 |
调用者清理 | syscall | 与cdecl类似,参数从右到左入栈。参数列表的大小放置在AL寄存器中。syscall是32位OS/2 API的标准。 |
调用者清理 | optlink | 参数也是从右到左压栈。从最右边开始的前三个参数会被放置在寄存器EAX、EDX和ECX中,最多四个浮点参数会被传入ST(0)到ST(3)中,虽然这四个参数的空间也会在参数列表的栈上保留。函数的返回值在EAX或ST(0)中。被保留的寄存器有EBP、EBX、ESI和EDI。optlink在IBM VisualAge编译器中被使用。 |
调用者清理 | pascal | 基于Pascal语言的调用约定,参数从左至右入栈(与cdecl相反)。被调用者负责返回前清理堆栈。此调用约定常见在16-bit API中、OS/2、较早Windows 3.x 以及Borland Delphi版本1.x。 |
调用者清理 | register | Borland fastcall的别名。 |
被调用者清理 | stdcall | 这是一个Pascal调用约定的变体,被调用者依旧负责清理堆栈,但是参数从右往左入栈与cdecl一致。寄存器EAX、ECX和EDX被按调用者约定使用,返回值放置在EAX中。stdcall对微软Win32 API和Open Watcom C++是标准。 |
被调用者清理 | fastcall | 此约定未被标准化,不同编译器的实现也不同。典型的fastcall约定会传递一个或多个参数到寄存器上,以减少对内存的访问。 |
被调用者清理 | Microsoft fastcall | Microsoft实现的fastcall约定,传递头两个参数(从左至右)到ECX和EDX中,剩下的参数从右至左压栈。 |
被调用者清理 | Borland fastcall | 从左至右,传入至少三个参数至EAX、EDX和ECX中。剩下的参数入栈,也是从左至右。在32位编译器Embarcadero Delphi中,这是默认的调用约定,在编译器中以register形式为人知。在i386上的某些版本Linux也使用了此约定。 |
调用者或被调用者清理 | thiscall | 在调用C++非静态成员函数时使用此约定。基于所使用的编译器和系统是否允许可变参数,有两种版本的thiscall。对于GCC编译器,thiscall几乎与cdecl等效,调用者清理栈空间,this 指针通过寄存器传递。对于Microsoft C++编译器,this 指针会放到ECX寄存器中,调用者负责清理栈空间。在较老的Windows API函数中,thiscall类似于stdcall约定。当函数使用可变参数时,调用者负责清理堆栈(类似cdecl)。微软编译器在Visual C++ 2005及其之后版本都支持它。其他编译器中,thiscall并不是一个关键字(反之,GNU 及MDA 使用__thiscall)。 |
关于C与ASM的混合编程办法
搞明白了C语言是如何协作参数调用的,那我们就可以猛猛的开始C和ASM混合调用了。常见的协调工作办法上,一种我们已经干过了:
现在,笔者来简单的小试一下。
/* here we provide a simple entry for asm print */
extern void assembly_print(const char* msg, const int msg_len);
void c_print(const char* msg)
{
int len = 0;
while(msg[len]){
len++; // fetch the length of a string
}
assembly_print(msg, len);
}
section .data
; the string we will print
demo_str: db "and this is is just a simple greetings!",0xA, 0
demo_str_len equ $-demo_str
section .text
; on the demo_c.c file
extern c_print
global _start
_start:
push demo_str
call c_print
add esp,4
mov eax,1
int 0x80
; tell the entern so ld could find the definitions
global assembly_print
assembly_print:
push ebp
mov ebp,esp
mov eax,4
mov ebx,1
mov ecx, [ebp+8]
mov edx, [ebp+12]
int 0x80
pop ebp
ret
这两个文件的难度都不大,但是有一个新面孔——int 0x80是什么呢?答案是我们请求了linux的系统调用。注意到,我们依次请求了Linux的四号系统调用和一号系统调用。分别是文件写和退出程序的调用。关于系统调用,笔者这里打算埋伏到后面的系统调用的实现上,慢慢讨论。
所有的代码都在CCOperateSystem/Documentations/4_Better_MainKernel/4.1_code at main · Charliechen114514/CCOperateSystem里了!,欢迎随时查看!
下一节
从0开始的操作系统手搓教程10:更好的内核2——实现我们自己的打印函数-CSDN博客