调用C函数
汇编调用C函数
从系统引导过程中的汇编程序跳转到系统主函数中,或者在中断处理的汇编代码中跳转到中断处理函数(传说中的中断上部), 这些过程都是从汇编程序跳转到C程序的,其中不可缺少的有:调用约定,参数传递方式,函数调用方式等。因为这些过程都是在系统内核中,所以,我们讲解的是GNU C语言和AT&T汇编语言。话不多说,下面让我们逐一介绍。
汇编调用C函数
函数的调用方式
函数的调用方式其实没那么复杂,基本上就是jmp、call、ret或者他们的变种而已。让我们先看下面的程序。
int test()
{
int i = 0;
i = 1 + 2;
return i;
}
int main()
{
test();
return 0;
}
这段程序基本上没有什么难点,很简单,对吧?唯一要注意的地方是main函数的返回值,这里个人建议大家要使用int类型作为主函数的返回值,而不要使用void,或者其他类型。虽然,
在主函数执行到return 0之后就跟我们没有什么关系了。但是,有的编译器要求主函数要有个返回值,或者,在某些场合里,系统环境会用到主函数的返回值。考虑到上述原因,要使用int类型作为主函数的返回值,如果处于某个特殊的或者可预测的环境下,那就无所谓了。
说了这么多,反汇编一下这段代码,看看汇编语言是怎么调用test函数的。工具objdump,用于反汇编二进制程序,它有很多参数,可以反汇编出各类想要的信息。
objdump工具命令:
objdump -d test
下面是反汇编后的部分代码,把相关的系统运行库等一些与上面C程序不相关的代码忽略掉。经过删减后的反汇编代码如下:
0000000000400474:
400474: 55  push %rbp
400475: 48 89 e5  mov %rsp,%rbp
400478: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40047f: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp)
400486: 8b 45 fc  mov -0x4(%rbp),%eax
400489: c9  leaveq
40048a: c3  retq
000000000040048b
:
40048b: 55  push %rbp
40048c: 48 89 e5  mov %rsp,%rbp
40048f: b8 00 00 00 00  mov $0x0,%eax
400494: e8 db ff ff ff  callq 400474
400499: b8 00 00 00 00  mov $0x0,%eax
40049e: c9  leaveq
40049f: c3  retq
大家先看000000000040048b :这一行,这里就是主函数,前面的000000000040048b其实是函数main的地址。一共16个数,16 * 4 = 64,对!这就是64位地址宽度啦。
乍一看,有好多个“%”符号,还记得2.2.1节里讲的AT&T汇编语法吗?这就是那里面说——引用寄存器的时候要在前面加“%”符号。
还有一些汇编指令的后缀,如:“l”、“q”。“l”的意思是双字(long型),“q”的意思是四字(64位寄存器的后缀就是这个)。
如果您仔细观察,是不是会发现有些寄存器rbp,rsp等,感觉会跟ebp和esp有关系呢?答对了,esp寄存器是32位寄存器,而rsp寄存器是64位寄存器。这是Intel对寄存器的一种向下继承性,从最开始一字节的al,ah,到两字节的ax(16位),四字节的eax(32位),再到八
字节的rax(64位),寄存器的长度在不断的扩展,对于相关指令的使用,也从“b”、“l”,“q”,也是不断的向下继承或扩展。
这里有一条指令leaveq,它等效于 movq %rbp, %rsp; popq %rbp;
callq 400474 这句的意思就是跳转到test函数里执行。其实汇编调用C函数就这么简单,如果把这条callq指令改成jmpq指令也是可以的。这要从call和jmp的区别上说起,call会把在其之后的那条指令的地址压入栈,在上面反汇编后的代码中,就是0000000000400499,然后再跳转到test函数里执行。而jmpq就不会把地址0000000000400499压入栈中。当函数执行完毕,调用retq指令返回的时候,会把栈中的返回地址弹出到rip寄存器中,这样就返回到main函数中继续执行了。
实现jmpq代替callq的伪代码如下所示:
pushq $0x0000000000400499
c语言编译器怎么用?
jmpq 400474
对于callq 400474 这条指令也可以使用retq来实现。它的实现原理是:指令retq会将栈中的返回地址弹出,并放入到rip寄存器中,然后处理器从rip寄存器所指的地址内取指令后继续执行。根据这个原理,可以先将返回地址0000000000400499压入栈中。然后再将test函数的入口地址0000000000400474压入栈中,接着使用retq指令,以调用返回的形式,从main函数“返回”到test函数中。
实现retq代替callq的伪代码如下所示:
pushq $0x0000000000400499
pushq $0x0000000000400474
retq
这些看起来是不是没有想象的那么难?其实把汇编的原理掌握清楚了,这些都是可以灵活运用的,希望这段内容能启发读者的灵感~!
调用约定
对于不同的公司,不同的语言以及不同的需求,都是用各自不同的调用约定,而且他们往往差异很大。在IBM兼容机对市场进行洗牌后,微软操作系统和编程工具占据了统治地位,除了微软之外,还有零星的一些公司,以及开源项目GCC,都各自维护着自己的标准。下面是比较流行的几款调用标准,咱们写的大多数程序都出自这个标准之一。
stdcall
1、在进行函数调用的时候,函数的参数是从右向左依次放入栈中的。
如:
int function(int first,int second)
这个函数的参数入栈顺序,首先是参数second,然后是参数first。
2、函数的栈平衡操作是由被调用函数执行的,使用的指令是 retn X,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。例如上面的function函数,当我们把function的函数参数压入栈中后,当function函数执行完毕后,由function函数负责将传递给它的参数first和second从栈中弹出来。
3、在函数名的前面用下划线修饰,在函数名的后面由@来修饰,并加上栈需要的字节数。如上面的function函数,会被编译器转换为_function@8。
cdecl
1、在进行函数调用的时候,和stdcall一样,函数的参数是从右向左依次放入栈中的。
2、函数的栈平衡操作是由调用函数执行的,这点是与stdcall不同之处。stdcall使用retn X平衡栈,cdecl则使用leave、pop、增加栈指针寄存器的数据等方法平衡栈。
3、每一个调用它的函数都包含有清空栈的代码,所以编译产生的可执行文件会比调用stdcall约定产生的文件大。
cdecl是GCC的默认调用约定。但是,GCC在x64位系统环境下,使用寄存器作为函数调用的参数。按照从左向右的顺序,头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上,同时XMM0到XMM7用来放置浮点变元,返回值保存在RAX中,并且由调用者负责平衡栈。
fastcall
1.函数调用约定规定,函数的参数在可能的.情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈。
2、函数的栈平衡操作是由被调用函数在返回之前负责清除栈中的参数。