从0开始的操作系统手搓教程9:更好的内核1——简单讨论一下C与ASM的混合编程

news/2025/2/22 17:40:21

现在,我们已经成功的进入了我们的内核。在之后更长的一段时间,我们可以使用更加高级的编程语言,也就是我们的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)。下面的整个表就表达了我们的常见的参数调用约定。笔者这里枚举一下:

常见调用约定
找清理负责任人类别描述
调用者清理cdeclcdecl(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。
调用者清理registerBorland fastcall的别名。
被调用者清理stdcall这是一个Pascal调用约定的变体,被调用者依旧负责清理堆栈,但是参数从右往左入栈与cdecl一致。寄存器EAX、ECX和EDX被按调用者约定使用,返回值放置在EAX中。stdcall对微软Win32 API和Open Watcom C++是标准。
被调用者清理fastcall此约定未被标准化,不同编译器的实现也不同。典型的fastcall约定会传递一个或多个参数到寄存器上,以减少对内存的访问。
被调用者清理Microsoft fastcallMicrosoft实现的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混合调用了。常见的协调工作办法上,一种我们已经干过了:

  • 我们将单独的C语言文件编译链接成目标文件,又将我们的单独的汇编文件汇编得到目标文件,两者在链接阶段的时候协调构成可执行程序

  • 我们在C语言源文件的时候直接嵌入我们的汇编代码。

现在,笔者来简单的小试一下。

/* 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博客


http://www.niftyadmin.cn/n/5862587.html

相关文章

【Excel】【VBA】根据内容调整打印区域

Excel VBA:自动调整打印区域的实用代码解析 在Excel中,我们经常需要调整打印区域。今天介绍一段VBA代码,它可以根据C列的内容自动调整打印区域。 Dim ws As Worksheet Dim lastRow As Long Dim r As Long 设置当前工作表 Set ws ActiveSh…

【目标检测】【YOLOv4】YOLOv4:目标检测的最佳速度与精度

YOLOv4:目标检测的最佳速度与精度 0.论文摘要 有许多特征被认为可以提高卷积神经网络(CNN)的准确性。需要在大规模数据集上对这些特征的组合进行实际测试,并对结果进行理论上的验证。某些特征仅适用于特定模型和特定问题&#…

网络安全入门持续学习与进阶路径(一)

职业认证与实战进阶 1 考取核心安全认证 认证名称适合人群考试要求考试时间/费用核心价值注意点CEH(道德黑客认证)渗透测试方向,入门级无强制经验要求,需参加官方培训(或自学)4小时,125题&…

基于云的物联网系统用于实时有害藻华监测:通过MQTT和REST API无缝集成ThingsBoard

论文标题 **英文标题:**Cloud-Based IoT System for Real-Time Harmful Algal Bloom Monitoring: Seamless ThingsBoard Integration via MQTT and REST API **中文标题:**基于云的物联网系统用于实时有害藻华监测:通过MQTT和REST API无缝集…

深度学习笔记16-VGG-16算法-Pytorch实现人脸识别

目录 前言 一、 前期准备 1. 设置GPU 2. 导入数据 3. 划分数据集 二、调用官方的VGG-16模型 三、 训练模型 1. 编写训练函数 2. 编写测试函数 3. 设置动态学习率 4. 正式训练 四、 结果可视化 1. Loss与Accuracy图 2. 指定图片进行预测 3. 模型评估 五、总结 前言 &#x1f368…

什么是矩阵账号?如何高效运营tiktok矩阵账号

‍‌​​‌‌​‌​‍‌​​​‌‌​​‍‌​​​‌​‌​‍‌​​‌​​‌​‍‌​‌‌​‌‌‌‍‌​‌​‌​​​‍‌​​‌​‌‌​‍‌​​​​‌‌​‍‌​‌​​‌‌‌‍‌​​‌‌​‌​‍‌​‌​​‌‌‌‍‌​‌‌‌​​‌‍‌‌​​‌‌‌​‍‌‌​​‌‌​​‍‌…

SpringBoot有几种获取Request对象的方法

HttpServletRequest 简称 Request,它是一个 Servlet API 提供的对象,用于获取客户端发起的 HTTP 请求信息。例如:获取请求参数、获取请求头、获取 Session 会话信息、获取请求的 IP 地址等信息。 那么问题来了,在 Spring Boot 中…

网络安全域管理 网络安全管理体系

🍅 点击文末小卡片 ,免费获取网络安全全套资料,资料在手,涨薪更快 信息安全管理体系 1、这些基于产品、技术与管理层面的标准在某些领域得到了很好的应用,但从组织信息安全的各个角度和整个生命周期来考察&#xff0c…