• 《操作系统-真象还原》06. 完善内核


    函数调用约定

    考虑如下问题:

    • 参数的传递方式:栈
    • 参数的传递顺序:从左到右?还是从右到左?
    • 由调用者负责清理栈空间还是被调用者清理栈空间?

    stdcall 调用约定

    int subtract(int a, int b); // 函数声明
    int sub = subtract(3, 2);   // 调用函数
    
    • 1
    • 2
    ; 主调用者
    ; ----------
    push 2		  ; 参数 b
    push 3		  ; 参数 a
    call subtract
    
    ; 被调用者
    ; ----------
    push ebp			; 备份 ebp
    mov ebp, esp		; 保存 esp, 用 ebp 来访问栈
    
    mov eax, [ebp + 0x8]; a
    add eax, [ebp + 0xc]; b
    
    ; 为了防止过程中 push 等其它操作使 esp 改变,可能导致最后无法正确 ret 返回
    ; 因此在此恢复 esp
    mov esp, ebp ; 可有可无, 因为中间没有修改 esp 的操作
    
    pop ebp ; 恢复 ebp
    ret 8   ; 先返回,然后 esp + 8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    stdcall 由被调用者清理栈空间。

    cdecl

    int subtract(int a, int b); // 函数声明
    int sub = subtract(3, 2);   // 调用函数
    
    • 1
    • 2
    ; 主调用者
    ; ----------
    push 2		  ; 参数 b
    push 3		  ; 参数 a
    call subtract
    add esp, 8	  ; 清理栈空间
    
    ; 被调用者
    ; ----------
    push ebp			; 备份 ebp
    mov ebp, esp		; 保存 esp, 用 ebp 来访问栈
    
    mov eax, [ebp + 0x8]; a
    add eax, [ebp + 0xc]; b
    
    ; 为了防止过程中 push 等其它操作使 esp 改变,可能导致最后无法正确 ret 返回
    ; 因此在此恢复 esp
    mov esp, ebp ; 可有可无, 因为中间没有修改 esp 的操作
    
    pop ebp ; 恢复 ebp
    ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    cdecl 由调用者清理栈空间。

    显卡端口

    内联汇编

    汇编的方式有:

    1. C 代码与汇编代码分别编译,最后通过链接成可执行文件。
    2. 在 C 代码中嵌入汇编代码,这就是“内联汇编”。
    3. 将C编译成汇编代码,接着再修改汇编代码。

    AT&T 语法

    AT&T 是汇编语言的一种语法风格、格式。并不是一种新的语言。
    image-20221002151707728

    在指令的末尾有个后缀,表示操作数大小,b:1bytew:2byte, l:4byte 。例如:push => pushl 表示压入 4 字节。

    寻址方式

    固定寻址格式:base_address(offset_address, index, size)
    对应表达式为:base_address + offset_address + index * size
    注意:括号内不用的参数,需要用逗号代替。

    • base_address:基地址
    • offset_address:偏移地址,必须是八大通用寄存器之一
    • index:索引值,必须是八大通用寄存器之一
    • size:因子,只能是 1、2、4、8(Intel 语法中也只能乘于这几个)

    直接寻址:base_address,例如:

    movl $255, 0xc00008F0
    
    • 1

    寄存器间接寻址:(offset_address),只能用通用寄存器存储 offset_address,存储的内容为地址。

    mov (%eax), %ebx
    
    • 1

    寄存器相对寻址:base_address(offset_address),也就是得到的内存为基地址+偏移地址之和。

    变址寻址:index * size,base_address 和 offset_address 可有可无。

    ; 无 base_address, 无 offset_address
    movl %eax, (,%esi, 2) ; 将 eax 的值写入到 esi*2 所指向的内存
    ; 无 base_address, 有 offset_address
    movl %eax, (%ebx, %esi, 2) ; 将 eax 的值写入到 ebx+esi*2 所指向的内存
    ; 有 base_address, 无 offset_address
    movl %eax, base_value(, %esi, 2) ; 将 eax 的值写入到 base_value+esi*2 所指向的内存
    ; 有 base_address, 有 offset_address
    movl %eax, base_value(%ebx, %esi, 2) ; 将 eax 的值写入到 base_value+ebx+esi*2 所指向的内存
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    基本内联汇编

    固定格式:asm [volatile] ("assembly code")

    • asmasm__asm__ 一样,都是由 GCC 定义的宏:#define __asm__ asm

    • volatile:是关键字,volatile__volatile__ 一样,都是由 GCC 定义的宏:#define __volatile volatile

      在 GCC 中有个 -O 参数,使用该参数编译时,GCC 会按照自己的意图优化代码,可能会因此修改了自己写的代码。volatile 关键字的作用是告诉 GCC 不要修改我写的汇编代码。

    • assembly code:编写汇编代码。

      1. 指令必须使用双引号引起来,无论是一条还是多条。
      2. 编写多条指令,则需要使用 \ 进行转义。
      3. 多条指令之间必须要有分隔符,除了最后一条指令。分隔符:;
    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; \
        	 popa");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    基本内联汇编的局限性:在基本内联汇编中,若要引用 C 变量,只能将其定义为全局变量。若定义为局部,则链接时会找不到这两个符号。

    扩展内联汇编

    格式:asm [volatile] ("assembly code" : output : input : clobber/modify)

    • output:作为汇编代码的输出,这样 C 才可以访问汇编的数据。

      格式:"操作数修饰符和约束名"(C 变量名)

    • input:作为汇编代码的输入,这样汇编才可以访问 C 的数据。

      格式:"[操作数修饰符] 约束名"(C 变量名)

    • clobber/modify:保护可能遭到破坏的一些寄存器或内存资源。

    寄存器约束
    • a:表示寄存器 eax/ax/al
    • b:表示寄存器 ebx/bx/bl
    • c:表示寄存器 ecx/cx/cl
    • d:表示寄存器 edx/dx/dl
    • D:表示寄存器 edi/di
    • S:表示寄存器 esi/si
    • q:表示任意这 4 个通用寄存器之一:eax/ebx/ecx/edx
    • r:表示任意这 6 个通用寄存器之一:eax/ebx/ecx/edx/esi/edi
    • g:表示可以存放到任意地点(寄存器或内存)
    • A:把 eax 和 edx 组成 64 位整数
    • f:表示浮点寄存器
    • t:表示第 1 个浮点寄存器
    • u:表示第 2 个浮点寄存器
    // 例:相加
    // 基本内联汇编
    int in_a = 1, in_b = 2, out_sum;
    void main() {
        asm("pusha; \
        	 movl in_a, %eax; \
        	 movl in_b, %ebx; \
        	 addl %ebx, %eax; \
        	 movl %eax, out_sum; \
        	 popa");
        printf("sum is %d\n", out_sum);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    // 扩展内联汇编
    #include 
    void main() {
        int in_a = 1, in_b = 2, out_sum;
        asm("addl %ebx, %eax" : "=a"(out_sum) : "a"(in_a), "b"(in_b));
        printf("sum is %d\n", out_sum);
    }
    // ---------------------
    output: 操作数修饰符为“=, 约束名为“a”, 变量名为“out_sum”
    input: 约束名为“a”, 变量名为“in_a” // 另一个同理
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    内存约束
    • m:把 C 变量的指针作为内联汇编代码的操作数。
    • o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset_address 的格式。
    #include 
    void main() {
        int in_a = 1, in_b = 2;
        printf("in_b is %d\n", in_b);
        asm("movb %b0, %1;" : : "a"(in_a), "m"(in_b));
        printf("in_b now is %d\n", in_b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image-20221002185255456

    立即数约束
    • i:表示操作数为整数立即数
    • F:表示操作数为浮点数立即数
    • I:表示操作数为 0~31 之间的立即数
    • J:表示操作数为 0~63 之间的立即数
    • N:表示操作数为 0~255 之间的立即数
    • O:表示操作数为 0~32 之间的立即数
    • X:表示操作数为任何类型立即数
    通用约束
    • 0~9:表示可与 output 和 input 中第 n 个操作数用相同的寄存器或内存。

    此约束只能用在 input 部分。

    序号占位符

    序号占位符是对在 output 和 input 中的操作数,按照它们从左到右出现的次序从 0 开始编号,一直到 9,也就是说最多支持 10 个序号占位符。

    操作数用在 assembly code 中,引导它的格式是 %0~9

    asm("addl %%ebx, %%eax" : "=a"(out_sum) : "a"(in_a), "b"(in_b));
    // 等价于
    asm("addl %2, %1" : "=a"(out_sum) : "a"(in_a), "b"(in_b));
    
    • 1
    • 2
    • 3
    名称占位符

    格式:[名称]"约束名"(C 变量)

    #include 
    void main() {
        int in_a = 18, in_b = 3, out = 0;
        asm("divb %[divisor]; movb %%al, %[result]" : [result]"=m"(out) : "a"(in_a), [divisor]"m"(in_b));
        printf("result is %d\n", out);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    操作数类型修饰符

    在 output 中:

    • =:表示操作数是只写。
    • +:表示操作数是可读可写。
    • &:表示此 output 中的操作数要独占所约束(分配)的寄存器,只供 output 使用,任何 input 中所分配的寄存器都不能与此相同。

    在 input 中:

    • %:该操作数可以和下一个输入操作数互换。

    扩展内联汇编 —— 机器模式

    机器模式用来在机器层面上指定数据的大小及格式。

  • 相关阅读:
    quarkus实战之五:细说maven插件
    NTU课程笔记:DeepLab
    攻克视频技术
    STM32微控制器的低功耗模式
    C语言基础知识
    【Python】设计模式
    【Vue】通过Axios实现异步通信(简单案例)
    【力扣刷题笔记第二期】Python 数据结构与算法
    机器学习平台架构系列-1-klubeflow
    在学习DNS的过程中给我的启发
  • 原文地址:https://blog.csdn.net/qq_43098197/article/details/127460076