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

函数调用约定

考虑如下问题:

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

stdcall 调用约定

int subtract(int a, int b); // 函数声明
int sub = subtract(3, 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

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

cdecl

int subtract(int a, int b); // 函数声明
int sub = subtract(3, 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

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

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

mov (%eax), %ebx

寄存器相对寻址: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 所指向的内存

基本内联汇编

固定格式: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");
}

基本内联汇编的局限性:在基本内联汇编中,若要引用 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);
}
// 扩展内联汇编
#include <stdio.h>
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” // 另一个同理
内存约束
  • m:把 C 变量的指针作为内联汇编代码的操作数。
  • o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset_address 的格式。
#include <stdio.h>
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);
}

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));
名称占位符

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

#include <stdio.h>
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);
}
操作数类型修饰符

在 output 中:

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

在 input 中:

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值