C语言和汇编语言函数调用

C语言和汇编语言函数调用关系

1.汇编语言函数调用

X86结构中,cs寄存器和rip寄存器共同控制着CPU要执行的下一条指令(当前在不同的模式中控制方式不同,如:实地址2模式和保护模式,长模式等),一般会按照指令在内存中存储的顺序,依次执行。如果想要在普通程序(除去系统调用和中断)执行中跳转到某一条指令,就需要使用JMPCALLRET及其变种指令。

  • jmp指令是无条件跳转指令,直接跳转到某条指令。
  • call指令在执行时,会先将下一条指令地址压栈,然后跳转到目标指令。
  • ret指令执行时,会从当前栈顶弹出目标指令的地址,然后跳转到目标指令。

利用上述相关指令进行搭配,就可以实现在一块汇编代码(a)执行中,跳转到另一块汇编代码(b),执行完(b)后返回到(a)的下一条指令继续执行;从而可以实现函数的调用和返回。

2.c语言函数调用

通过分析c语言编译->反汇编后汇编代码,可以看出c语言的调用方式,注意不同的编译环境调用机制可能不一样,实验环境是gcc 4.4.7

1.测试代码
int add(int a,int b){
    int c = a+b;
    return c;
}

void main(){
    int i = 0;
    int j = 1;
    add(i,j);
}
2.反汇编后的汇编代码

栈的生长方向是由高地址到低地址,即压栈时(pop),rsp寄存器会减小;出栈时(push),rsp寄存器会增大。

leaveq等效于movq %rbp, %rsp; popq %rbp;这条指令的主要为了对应函数入口处的push %rbp;mov %rsp,%rbp。

000000000000009a : ;add函数机器码和对应的汇编代码(64位)
; 地址   机器码                   汇编代码
  9a:	55                   	push   %rbp
  9b:	48 89 e5             	mov    %rsp,%rbp
  9e:	89 7d ec             	mov    %edi,-0x14(%rbp)
  a1:	89 75 e8             	mov    %esi,-0x18(%rbp)
  a4:	8b 45 e8             	mov    -0x18(%rbp),%eax
  a7:	8b 55 ec             	mov    -0x14(%rbp),%edx
  aa:	8d 04 02             	lea    (%rdx,%rax,1),%eax
  ad:	89 45 fc             	mov    %eax,-0x4(%rbp)
  b0:	8b 45 fc             	mov    -0x4(%rbp),%eax
  b3:	c9                   	leaveq 
  b4:	c3                   	retq   

00000000000000b5 
: ;main函数机器码和对应的汇编代码(64位) ; 地址 机器码 汇编代码 b5: 55 push %rbp b6: 48 89 e5 mov %rsp,%rbp b9: 48 83 ec 10 sub $0x10,%rsp bd: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) c4: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) cb: 8b 55 fc mov -0x4(%rbp),%edx ce: 8b 45 f8 mov -0x8(%rbp),%eax d1: 89 d6 mov %edx,%esi d3: 89 c7 mov %eax,%edi d5: 48 b8 9a 00 00 00 00 mov $0x9a,%rax dc: 00 00 00 df: ff d0 callq *%rax e1: 89 45 f8 mov %eax,-0x8(%rbp) e4: c9 leaveq e5: c3 retq

分析上述反汇编后的汇编代码。

main函数和add函数的入口部分代码和出口部分代码都是:

;函数入口
push %rbp   ;保存rbp的值
mov %rsp,%rbp ;将当前栈顶数值(a)->rbp寄存器
;中间部分,不会改变rbp寄存器的值
;函数出口
;以下2条汇编代码等同于leaveq
mov %rbp,%rsp ;将rbp寄存器的数值(a)->rsp,现在栈顶的数就是入口保存的rbp值
popq %rbp ;将栈顶值恢复到rbp寄存器

可以得出所有c语言函数的入口和出口部分都会保存和恢复rbp的值,并且在保存rbp旧值到当前栈顶后,会将当前栈顶赋值给rbp;阅读函数中间部分汇编代码,发现不会改变rbp的值了,这样的话不管函数中间部分如何改变rsp的值(可能该函数会使用push或pop指令,或者使用call指令进行了压栈等等可以改变rsp寄存器的值的指令),最终都可以在函数出口时,将栈环境恢复到入口未执行时的栈环境(栈环境指:rsp和rbp寄存器的值,还有栈中的值)。

add函数中间部分:

mov    %edi,-0x14(%rbp) ;保存第一个参数的值
mov    %esi,-0x18(%rbp) ;保存第二个参数的值
mov    -0x18(%rbp),%eax
mov    -0x14(%rbp),%edx
lea    (%rdx,%rax,1),%eax
mov    %eax,-0x4(%rbp) ;保存局部变量c的值
mov    -0x4(%rbp),%eax ;保存返回值到eax

首先rbp在入口部分结束时,被赋予了新值(当前栈顶),其次因为栈时由高地址向低地址扩展;所以上述代码等于先将edi和esi的值保存到内存栈扩展方向的某2个位置上(rbp-0x14,rbp-0x18),然后将内存中这两个值读出来进行运算,完成后保存到内存栈扩展方向的另一个位置上(rbp-0x4);通过对比add函数的c语言代码,可以发现内存(rbp-0x14,rbp-0x18)位置最终保存该函数的2个输入参数,内存(rbp-0x4)位置,最终保存局部变量c;由此得出,寄存器edi,esi分别为add传入了2个参数,寄存器eax保存了返回参数。

观察main函数中间部分,来验证上述假设:

sub    $0x10,%rsp 		;rsp=rsp-0x10 使得在callq调用时,将下一条指令地址保存到(rsp-0x10)位置
movl   $0x0,-0x8(%rbp)	;保存第一个局部变量i的值0
movl   $0x1,-0x4(%rbp)	;保存第二个局部变量j的值1
mov    -0x4(%rbp),%edx	
mov    -0x8(%rbp),%eax	
mov    %edx,%esi		;将j的值保存到esi中(第二个参数)
mov    %eax,%edi		;将i的值保存到edi中(第一个参数)
mov    $0x9a,%rax       ;将add函数的入口地址保存到rax中

callq  *%rax            ;调用add函数(将下一条指令地址压栈,然后跳转到0x9a处执行,即跳到add函数执行第一条指令)
mov    %eax,-0x8(%rbp)  ;将返回值赋值给i,(rbp-0x8)位置代表变量i

当改变add函数的输入参数为一个float变量和一个int变量时:

int add(float a,int b){

    int c = a+b;

    return c;

}

查看反汇编后的汇编代码:

0000000000000027 :
  27:	55                   	push   %rbp
  28:	48 89 e5             	mov    %rsp,%rbp
  2b:	f3 0f 11 45 ec       	movss  %xmm0,-0x14(%rbp)
  30:	89 7d e8             	mov    %edi,-0x18(%rbp)
  33:	f3 0f 2a 45 e8       	cvtsi2ssl -0x18(%rbp),%xmm0
  38:	f3 0f 58 45 ec       	addss  -0x14(%rbp),%xmm0
  3d:	f3 0f 2c c0          	cvttss2si %xmm0,%eax
  41:	89 45 fc             	mov    %eax,-0x4(%rbp)
  44:	8b 45 fc             	mov    -0x4(%rbp),%eax
  47:	c9                   	leaveq 
  48:	c3                   	retq   

可以发现函数会从xmm0寄存器获取输入的float参数。
add函数和mian函数返回时,都使用了下述指令:

retq 

该指令时是64位时,ret指令的变种。因为执行到该指令时,被调用函数的出口部分已经执行完毕,栈恢复到调用者刚进入时的环境,被调用者执行ret指令将会弹出当前rsp指向的栈顶的指令地址,然后跳转到该指令执行。所以在a函数调用b函数时,要确保跳转到b入口处时,当前rsp指向的栈顶保存了a函数在b函数返回后想要指行的指令地址。返回值保存到了寄存器eax中。

3.总结

如果将调用者mian函数callq指令和被调用者add函数的retq指令联系起来,如下:

00000000000000b5 
: ;main函数机器码和对应的汇编代码(64位) ; 地址 机器码 汇编代码 b9: 48 83 ec 10 sub $0x10,%rsp ;将rsp栈指针赋予新值newRsp,相当于初始化了一个空栈newStack ;........... d5: 48 b8 9a 00 00 00 00 mov $0x9a,%rax dc: 00 00 00 df: ff d0 callq *%rax ;将mian函数下一条指令地址(0xel)压到栈newStack,然后跳转到(0x9a)执行 e1: 89 45 f8 mov %eax,-0x8(%rbp) ;add函数retq执行后,跳回到该指令继续执行main函数。 ;........... e4: c9 leaveq ;mian函数执行完成后,恢复到main函数入口时的栈环境,同时销毁了newStack(rsp = rbp) ;........... 000000000000009a : ;add函数机器码和对应的汇编代码(64位) ; 地址 机器码 汇编代码 9a: 55 push %rbp ;将mian函数的rbp保存到新栈newStack 9b: 48 89 e5 mov %rsp,%rbp ;让rbp指向新栈newStack栈顶 ;........................ b3: c9 leaveq ;add业务代码执行完成,恢复到入口时的新栈newStack环境,此时新栈newStack中只保存了(0xel),即main下一条指令地址 b4: c3 retq ;弹出0xel,并跳转到(0xel)执行,此时rsp=newRsp,恢复栈环境到mian函数未执行Call时。

当执行一个c语言函数X时,都会在执行到函数X的第一条指令前,初始化好一个栈(rsp减去一个栈单位的地址)是当前函数X的栈基址—减去一个栈单位,是因为在函数X退出时(ret)会额外在栈中弹出一个栈单位的数作为返回地址,因为ret指令也是在当前函数X中执行,因此函数X的栈基地址应该为函数X执行第一条指令前的(rsp减去一个栈单位的地址)

栈底部保存着函数的返回地址(即函数X执行完后,要返回到哪个地址处执行指令)和函数X入口时的rbp值,紧接着保存函数X会使用到的局部变量和函数参数。函数X如果有输入参数,则按照从左向右的顺序依次将通用寄存器RDI、RSI、RDX、RCX、R8和R9中的参数值保存到栈中;同时,寄存器XMM0~XMM7用来获取浮点参数,而RAX寄存器则用于保存函数的返回值;因为函数的所有参数还有局部变量都会被保存到该函数的栈中,在参数保存到栈中后,对于这些寄存器就可以随意使用,因为对于变量和参数的赋值都会直接通过进行。

函数X执行结束返回后,会使当前的rsp指向函数 X的栈基址
C语言和汇编语言函数调用_第1张图片

3.参考

AMD64 Architecture Programmer’s Manual, Volume 1: Application Programming

一个64位操作系统的设计与实现 第 2 章 环境搭建及基础知识

自制操作系统GitHub地址

你可能感兴趣的:(自制操作系统,linux,操作系统,c语言,反汇编)