函数调用约定
汇编语言和C语言的混合编程
C库函数与系统调用
系统调用与BIOS下中断的联系和区别:
使用系统调用:
#include<unistd.h>
int main()
{
write(1,"hello,world\n",11);
}
push 11 push str push 1
call + 函数名(write)
add esp,12
//此时write在内部实现中又作为主调函数(C语言),来调用函数(汇编语言),根据系统真正的系统调用需要的参数来压栈
push xxx
call true_write //然后调用 系统调用的汇编代码(代码为)
add esp xxx
//真正的系统调用汇编为:
true_write:
push xxx //压栈保护
mov eax,[esp+xx] //从栈中取功能号
mov xxx,[esp+xx]
int 0x80
mov eax,1 //退出
int 0x80
pop xxx
汇编语言与C语言可以互相调用
实现单个字符打印函数:put_char(被调函数)
2、获取光标值,这是下一个可打印字符的位置,即这次打印的位置
3、获取打印的字符
4、若字符是回车/换行符:坐标值减去除以80取余后的值得到行首坐标,加上80得到下一行行首新的坐标值。(可能滚屏)
若字符是退格符:将坐标值减一后将那处的字符设为0x20(空格),得到新的坐标值。
若字符是普通字符:将坐标值处设为预定字符后,坐标值加一得到新的坐标值。(可能滚屏)
5、判断新的坐标值是否大于2000,若大于2000做滚屏处理后得到新的坐标值
6、将新的坐标值写入写入光标寄存器,使其指向下一个可打印字符的位置
7、恢复寄存器现场,退出
TI_GDT equ 0
RPL_0 equ 0
SELETOR_VIDEO equ (0x3<<3)+TI_GDT+RPL_0
section .data
put_int_buffer dq 0
[bits 32]
section .text
global put_char ;此函数是被调函数,所以参数已经被主调函数压入栈中
put_char:
pushad
mov ax,SELETOR_VIDEO ;放在函数中目的:用户调用完成后,当内核态便会用户态后CPU会将gs置0的,所以我们这里要在内核态情况下置1。
mov gs,ax
;获得光标高8位,储存在al中
mov dx,0x3d4 ;寄存器组的索引寄存器
mov al,0Eh
out dx,al
mov dx,0x3d5h ;寄存器组的数据寄存器
in al,dx
mov ah,al
;获得光标低8位,储存在al中
mov dx,0x3d4
mov al,0Fh
out dx,al
mov dx,0x3d5h
in al,dx
;获得字符,这个函数被被调函数,直接去栈上面取字符即可
mov bx,ax ;基址寻址,习惯用bx
mov cl,[esp+36]
cmp cl,0xd ;CR
jz .is_carriage_return
cmp cl,0xa ;LF
jz .is_line_feed
cmp cl,0x8 ;backspace
jz .is_backspace
jmp .put_other
.is_carriage_return
.is_line_feed
;bx中存储的是光标值,bx*2得到偶地址为对应显示字符的字符值
;行首光标值加上80得到下一次光标值
xor dx,dx
mov ax,bx
mov si,80
div si ;dx:ax/80=ax:dx,得到了行数
sub bx,dx ;减去余数得到了光标行数坐标值
add bx,80 ;bx中得到新的光标值,要和2000比较,小于时正常设置
cmp bx,2000
jl .set_cursor ;大小于时候滚屏
jmp .roll_screen
.roll_screen
cld ;大数据转移三剑客:cld (esi,edi) rep movs[bwd]
mov ecx,960 ;2000-80=1920,1920*2=3840,每次搬运4字节
mov esi,0xb80a0 ;第1行行首
mov edi,0xb8000 ;第0行行首
rep movsd ;每次搬运完成后,esi加4,edi加4,ecx-1,和0比较直到相等为止。
;最后一行置0
mov ebx,3840
mov ecx,80
mov esi,0
mov eax,0x0720
.cls:
mov word[ebx+esi],eax ;内存由高地址往高地址赋值:ecx,ebx,esi,ecx,loop
add esi,2
loop .cls
mov ebx,1920
set_cursor:
;先设置高8为 bx中已经是更新的坐标值
mov dx,0x03d4
mov al,0x0e
out dx,al
mov dx,0x03d5
mov al,bh
out dx,al
;再设置低8位
mov dx,0x03d4
mov al,0x0e
out dx,al
mov dx,0x03d5
mov al,bl
out dx,al
.put_char_done
popad
ret
实现字符串打印:put_str(被调函数)
2,打印字符函数一次打印一个字符:将要打印的字符从地址处取出来,然后比较和0的大小,若和0不相等:压栈,然后call。
3,若和0相等,终止打印。因为是被调函数,要做好被调函数的工作:ret
global put_str
put_str:
pushad
mov ebx,dword[esp+36]
mov esi,0
xor eax,eax
.loop_char
mov al,[ebx+esi]
inc esi
cmp al,0
jz .str_over
push eax
call .put_char
add esp,4
jmp .loop_char
.str_over
popad
ret
实现整数打印:put_int(被调函数)
实现的功能是:当我们输入8位16进制的数字,打印出来对应的字符,如输入0x12345678,转换为机器码后是小端顺序78 56 34 12,要打印出来'1','2','3','4','5','6','7','8'八个字符,我们输入的共4个字节,一个字节两位,所以我们输入一个字节的要转换成两个字节,所以我们输入的要转换成为8个字节,由大端顺序储存。
我们调用时候的格式是C语言是主调函数put_int(0x12345678),将12345678压入4字节的栈,小端储存,则在内存中为:78 56 34 12。因为要把4字节分别分为8字节的,所以我们需要提前备好8个字节的缓冲区,然后开始在4字节的栈中取出一字节8位:0x78,然后转化成两个字符 '7' '8'。。。依次转换后存到缓冲区中高字节,在内存中的顺序变为:'1' ‘2’ ‘3’ ‘4’ ‘5’ ‘6’ ‘7’ ‘8’。然后存完后开始打印,打印时候要判断前几位是否为0,为0的要删除:0000123-----123
global put_int
.put_int
pushad
mov ebx,put_int_buffer ;数字缓冲区起始地址
add ebx,8 ;ebx指向缓冲区最顶端,从上往下减
mov ebp,esp+36 ;将数字的起始地址给ebp
mov esi,0 ;数字栈区往上加
xor ecx,ecx
mov ecx,4 ;主调函数压栈4个字节,循环4次
xor eax,eax
xor edx,edx
.loop_get_byte
dec ebx
mov al,[ebp+esi] ;;将第一个字节给移动到al中
mov dl,al ;分开al,dl存高4位,al存低4位
and al,0000_1111b ;高4位置0,低4位不变 8
shr dl,4 ;高4位置0,低4位不变 7
add al,'0'
add dl,'0'
mov [ebx],al
mov [ebx-1],dl
inc esi
loop .loop_get_byte
;现在ebx指向缓冲区起始地址
;开始处理高位连续0字符, 000123--123
mov esi,0
xor eax,eax
.loop_zero
mov al,[ebx+esi]
inc esi
cmp al,'0'
jz .loop_zero ;和‘0’字符比较,相等继续循环
jmp .print_int ;不等则说明可以打印,调转
.print_int
xor eax,eax
xor ecx,ecx
xor edx,edx
dec esi
mov edx,8
sub edx,esi
mov ecx,edx ;打印次数
.loop_print
mov eax,[ebx+esi] ;此时该地址的字符可以打印了
inc esi
push eax
call .put_char
add esp,4
loop .loop_print
内联汇编
GCC支持在C代码中直接嵌入汇编代码,所以叫做GCC inline assembled。内联汇编中所用的汇编语言,语法是AT&T。而我们常用的是nasm支持的Intel语法。
AT&T
操作数方面:
Intel的操作数想要成为内存地址,要用【】括起来。AT&T认为,操作数就是地址,所以遇到操作数时优先认为是地址,若要当成操作数,要加上$
内存寻址方面:
AT&T:segreg(段基址):bese_address(offset_address,index,size)==segreg:bese_address+offset_address+index*size
Intel:segret:[base+index*size+offset]
base_address是基地址,可以是整数、变量名
offset_address是偏移地址,index是索引值,这两个必须是8个通用寄存器之一。
size只能是1,2,4,8
直接寻址:
只有base_address eg: movl $222,0xc000_0000
寄存器间接寻址:
寄存器相对寻址:
变址寻址:
利用Index中的来。既然是变址,只有index和size即可,其他可有可无,没有的保留逗号来占位。movl %eax,(,%esi,2)
扩展内联汇编
asm [ volatile ] ( " assembly code ":output : intput : clobber / modify)
assembly code:用户写入的汇编指令。
output : inpt:这是汇编和C语言交互的关键。正式C为汇编提供输入参数和存储其输出的部分。
output:用来指定汇编代码的数据如何输出给C代码使用,格式: “操作数修饰符约束名”(C变量名)
input:用来指定C中数据如何输入给汇编使用。格式: “ [操作数修饰符] 约束名 “(C变量名)
clobber/modify:汇编代码执行后可能破坏一些寄存器或者内存资源,通过此项,可以让编译器提前将这些资源保存起来。
assembly code中引用的所有操作数其实是经过GCC转换后的副本,”原件“都是在output和input括号中的C变量。
扩展内联汇编中的”约束“,起的作用就是把C代码中的操作数(变量、立即数)映射为汇编中使用的操作数,实际描述的就是C中的操作数怎么样变成汇编的操作数。
约束分为四大类:
1.寄存器约束
就是要求GCC使用哪个寄存器,将input和output中变量约束在某个寄存器中。常见的寄存器约束有:
a:代表eax/ax/al b:代表ebx/bx/bl c:代表ecx/cx/cl d:代表edx/dx/dl D:代表edi/di S:代表esi/si q:代表eax/ebx/ecx/edx中任意一个 r:代表eax/ebx/ecx/edx/esi/edi任意
eg:
void main()
{
int in_a=1,in_b=2,out_sum;
asm( " addl %%ebx , %%eax : " = a " (out_sum) : " a " (in_a) , " b "( in_b ) " ) ;
}
可以看到寄存器约束的作用:顺序永远都是先输入,然后代码,然后输出。
2,内存约束
要求GCC直接将位于input和output中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写。
m:表示操作数可以使用任意一种内存形式。
o:操作数为内存变量,但访问它是通过偏移量的形式访问
void main()
{
int in_a=1,in_b=2,out_sum;
asm( " addl %b0 , %1 ;” : : " a " (in_a) , " m "( in_b ) " ) ;
}
%0和%1是指占位符,%b0的b代表byte,低8个字节。
3,立即数约束
GCC传值时候不通过内存或寄存器,直接作为立即数传给汇编代码,只能放在input中。
i:表示操作数为整数立即数
F:表示操作数为浮点立即数
4,通用约束
0-9:此约束只用在input部分。
占位符:序号占位符和名称占位符两种。
序号占位符:是对在output和intput中的操作数,按照它们从左到右出现的次序从0开始编号,一直到9。操作数用在assembly code中,引用它的格式是%0-9。在%和序号之前插入字符 ‘ h ’ 表示操作数为ah,插入字符 ‘ b ‘来表示操作数al
占位符只表示C变量经过约束后,由GCC分配的对应于汇编代码中的操作数,和C变量本身无关。就是汇编中的寄存器、内存和立即数。
output:
=:表示操作数是只写,相当于为output括号中的C变量赋值。
+:表示操作数是可读写的,告诉GCC所约束的寄存器或内存先被读入,再被写入。即既可以作为输入,也可以作为输出,省去了在input中声明约束。
&:表示output中的操作数要独占所约束的寄存器,只供output使用。
input:
%:该操作数可以和下一个输入操作数互换。
扩展内联汇编之机器模式简介
机器模式是用来在机器层面上指定数据大小及格式。
h-输出寄存器高位部分中的那一字节对应的寄存器名称。eg:ah、bh、ch、dh
b-输出寄存器中低部分1字节对应的名称,eg:al、bl、cl、dl
w-输出寄存器中大小2个字节对应的部分,eg:ax、bx、cx、dx
k-输出寄存器的四字节部分,eg:eax ebx ecx edx