Ⅰ.函数调用约定
首先,函数调用在处理器执行过程中实际是栈的切换,从当前执行任务的栈切换到另一个栈,但是切换过程中涉及到参数传递方式、参数传递顺序、栈的销毁等,因此在切换过程中需要明确函数调用约定。
根据下面表可看出在函数调用过程中主要规定了参数入栈方式(根据栈先入后出原则,一般从右向左)、栈清理方式(被调用者清理-函数执行完了自己清理、调用者清理-调用返回后清理,实质就是将栈指针恢复调用之前)等。
以stdcall为例,说明调用全过程:
(1)从右到左入栈
(2)被调用函数栈清理
int subtract(int a, int b); //被调用者
int sub= subtract (3,2); //主调用者
主调用者:
;从右到左将参数入校
push 2 ;压入参数 b
push 3 ;压入参数 a
call subtract ;调用函数 subtract
被调用者:
push ebp ;备份主调用者的栈
mov ebp, esp ;更新被调用者的栈指针esp
mov eax,[ebp+0x8] ;取a
add eax,[ebp+0x] ;取b
pop ebp ;恢复ebp
ret 8 ;标识返回后ret+8,清理栈
以cdecl调用约定为例,说明调用全过程:
(1)从右到左入栈,允许参数不固定
(2)主调用函数栈清理
int subtract(int a, int b); //被调用者
int sub= subtract (3,2); //主调用者
主调用者:
;从右到左将参数入校
push 2 ;压入参数 b
push 3 ;压入参数 a
call subtract ;调用函数 subtract
add esp, 8 ; 回收(清理)栈空间
被调用者
push ebp ;备份主调用者的栈
mov ebp, esp ;更新被调用者的栈指针esp
mov eax,[ebp+0x8] ;取a
add eax,[ebp+0x] ;取b
pop ebp ;恢复ebp
ret ;恢复ebp
Ⅱ.汇编语言和C语言混合模型
汇编语言和 C 语言混合编程可分为两大类。
(1)单独的汇编代码文件与单独的 C 语言文件分别编译成目标文件后, 一起链接成可执行程序。
(2)在 C 语言中嵌入汇编代码,直接编译生成可执行程序。
BIOS中断向量表的每一个中断号对应一个功能,而中断描述符表的每一个中断号可以对应不同的子功能。系统调用的入口只有一个,即第 0x80 号中断,具体的子功能在寄存器 eax中单独指定 。
1.系统调用
(1)调用“系统调用”有两种方式。
- 将系统调用指令封装为 c 库函数,通过库函数进行系统调用,操作简单。
- 不依赖任何库函数,直接通过汇编指令 int 与操作系统通信。
当输入的参数小于等于 5 个时, Linux 用寄存器传递参数。当参数个数大于 5 个时,把参数按照顺序放入连续的内存区域,并将该区域的首地址放到 ebx 寄存器。这里我们只演示参数小于等于 5 个的情况。
eax 寄存器用来存储子功能号(寄存器 eip 、 ebp 、 esp 是不能使用的)。 5 个参数存放在以下寄存器中,传送参数的顺序如下。
(1) ebx 存储第 1 个参数。
(2) ecx 存储第 2 个参数。
(3) edx 存储第 3 个参数。
(4) esi 存储第 4 个参数。
(5) edi 存储第 5 个参数。
2.内联汇编
(1)含义以及语法介绍
内联汇编:在C语言中嵌入汇编代码。针对不同平台,使用不同的汇编代码规范,譬如windows下使用Intel语法,而在Linux下,使用AT&T语法。Intel语法更符合高级语言编写风格,如mov eax, ebx表示eax=ebx;而AT&T语法更符合处理器处理方式,mov eax, ebx表示ebx=eax。
AT&T寻址方式
segreg (段基址): base address(offset_address,index,size)
等价于segreg (段基址): base_address+ offset_address+ index*size
(2)内联汇编形式
基本内联汇编
asm [volatile] (”assembly code")
下面说下 assembly eode 的规则。
( l )指令必须用双引号引起来,无论双引号中是一条指令或多条指令。
(2 ) 一对双引号不能跨行,如果跨行需要在结尾用反斜杠气’转义。
(3 )指令之间用分号";"、换行符’\n’或换行符加制表符’\n\t’分隔。那么就不能再内联汇编里写注释了
即使是指令分布在多个双引号中, gcc最终也要把它们合并到一起来处理,合并之后,指令间必须要有分隔符。所以,当指令在多个双引号中时,除最后一个双引号外,其余双引号中的代码最后 一定要有分隔符。
小试牛刀:在C语言中内嵌汇编,实现打印字符串功能:
char str[10] = "hello world\n";
int len = 10;
int ret = 0;
void main(void){
asm volatile ("pusha; # 将所有变量压入栈"
"movl $4,%eax; # 中断子功能号"
"movl $1,%ebx; # fd=1表示通过stdout输出"
"movl str,%ecx;"
"movl len,%edx;"
"int $0x80;"
"movl %eax, ret;"
"popa;"
);
printf("result is : %d", ret);
}
等同于
char str[10] = "hello world\n";
int len = 10;
int ret = 0;
void main(void){
asm volatile ("pusha;\
movl $4,%eax;\
movl $1,%ebx;\
movl str,%ecx;\
movl len,%edx;\
int $0x80;\
movl %eax, ret;\
popa;\
);
printf("result is : %d", ret);
}
扩展内联汇编
基本内联汇编可能只嵌入C语言的一部分,编译后需要分配寄存器资源,那么如何确保汇编调用的寄存器未被占用?此外,若汇编代码需要用到C语言变量,如何找到可用的寄存器保存操作数呢?于是,衍生了扩展内联汇编。具体方法类似让内联汇编像函数一样调用。
类似。通过添加约束,规定了操作数使用的寄存器
asm volatile {"assembly code" : output : input : clobber/modify}
- input可引入计算过程中涉及到的c变量,使用规则为:
“[操作数修饰符]约束名”(C 变量名)
- output将计算结果保存到c变量,使用规则为:
“操作数修饰符约束名”(C 变量名)
- clobber/modify :汇编代码执行后会破坏一些内存或寄存器资源,通过此项通知编译器,可能造成寄
存器或内存数据的破坏,这样 gee 就知道哪些寄存器或内存需要提前保护起来
(3)扩展内联汇编约束
总共包括四大类约束:寄存器约束、内存约束、
(3.1)寄存器约束
寄存器约束就是要求 gcc使用哪个寄存器,将 input 或 output 中变量约束在某个寄存器中。常见的寄存器约束有:
a:表示寄存器 eax/ax/al
b:表示寄存器 ebx/bx/bl
c:表示寄存器 eex/ex/cl
d:表示寄存器 edx/dx/dl
D :表示寄存器 edi/di
S :表示寄存器 esi/si
q:表示任意这 4 个通用寄存器之-: eax/ebx/ecx/edx
r:表示任意这 6 个通用寄存器之一 :eax/ebx/ecx/edxesi/edi
g:表示可以存放到任意地点(寄存器和内存)。相当于除了同 q 一样外,还可以让 gcc 安排在内存中
A :把 eax和 edx 组合成 64 位整数
f:表示浮点寄存器
t :表示第 1 个浮点寄存器
u:表示第 2 个浮点寄存器
示例:加法操作
基本内联汇编
#include <stdio.h>
int in_a = 1, in_b = 2;
int ret;
void main(void){
asm volatile {"pusha;\
movl in_a,%eax;\
movl in_b,%ebx;\
addl %ebx,%eax;\
movl %eax,ret;\
popa;"}
printf("result is %d", ret);
}
扩展内联汇编
#include <stdio.h>
int in_a = 1, in_b = 2;
int ret;
void main(void){
asm volatile {"addl %%ebx,%%eax":"=a"(ret):"a"(in_a),"b"(in_b)}
printf("result is %d", ret);
}
扩展内联汇编中寄存器前缀是两个%。
input:a表示用eax存储in_a,b表示用ebx存储in_b
output:=号表示操作数类型修饰符,表示只写,a表示eax
(3.2)内存约束
内存约束是要求 gcc 直接将位于 input 和 output 中的 C 变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是 C 变量的指针。
(3.3)立即数约束
立即数即常数,此约束要求 gee 在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码 。由于立即数不是变量,只能作为右值,所以只能放在 input 中。
i:表示操作数为整数立即数
F:表示操作数为浮点数立即数
I :表示操作数为 0~ 31 之间的立即数
J:表示操作数为 0~ 63 之间的立即数
N:表示操作数为 0~255 之间的立即数
O:表示操作数为 0~32 之间的立即数
X:表示操作数为任何类型立即数
(3.4)通用约束
0~ 9:此约束只用在 input 部分,但表示可与 output 和 input 中第 n 个操作数用相同的寄存器或内存 。
总结:
首先在实模式下配置好硬盘读写的配置,设置相应的LAB地址、读写操作、读入状态寄存器,确保MBR能够从指定的硬盘地址加载loader.s到内存中。
执行loader.s,主要完成实模式切换到保护模式,设置内存管理方式(分页或分段)。模式切换:设置gdt表;设置cr0寄存器,开启A20地址线。内存管理方式设置:初始为分段,页面置换以段为单位,但是在小内存的情况下,以段为单位进行置换也可能会出现内存不够放置段的情况,因此想要将段划分为更小的单位——页。设置cr3 gdt表存储寄存器和cr0寄存器;设置页目录表、页表、页表项;重新放置gdt。进入保护模式后,需要从新设置gdt地址,因为实模式下内存空间只有1MB,在保护模式下,0地址存放的是内核,为了保证内核有足够的空间,所以需要将gdt表重新布局。
执行main.c。完成内核映像加载到虚拟内存的3G高址空间,但是不能从3G开始,因为3G空间起始部分存放的是loader程序以及gdt表,根据loader最多不超过2000字节,通过计算后可以得到映像最合适的存放位置。
系统调用本质是中断,系统调用方式有两种:1.将系统调用的int指令封装在c中库函数供开发者使用;2.直接调用int指令与操作系统通信。
本文讲解了内联汇编的两种方式:基本内联汇编,扩展内联汇编。基本内联汇编存在缺点为,执行处寄存器状态未知以及可用寄存器信息未知,扩展内联汇编通过添加多种约束来保证汇编执行过程中使用的寄存器。
存的3G高址空间,但是不能从3G开始,因为3G空间起始部分存放的是loader程序以及gdt表,根据loader最多不超过2000字节,通过计算后可以得到映像最合适的存放位置。
系统调用本质是中断,系统调用方式有两种:1.将系统调用的int指令封装在c中库函数供开发者使用;2.直接调用int指令与操作系统通信。
本文讲解了内联汇编的两种方式:基本内联汇编,扩展内联汇编。基本内联汇编存在缺点为,执行处寄存器状态未知以及可用寄存器信息未知,扩展内联汇编通过添加多种约束来保证汇编执行过程中使用的寄存器。