打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
汇编调用C函数

来自: 图灵社区

内容选自图灵社区电子书《一个64位操作系统的实现》

作者:田宇 

链接:http://www.ituring.com.cn/article/192591(点击尾部阅读原文前往)


比如:从系统引导过程中的汇编程序跳转到系统主函数中,或者在中断处理的汇编代码中跳转到中断处理函数(传说中的中断上部), 这些过程都是从汇编程序跳转到C程序的,其中不可缺少的有:调用约定,参数传递方式,函数调用方式等。因为这些过程都是在系统内核中,所以,我们讲解的是GNU C语言和AT&T汇编语言。话不多说,下面让我们逐一介绍。


函数的调用方式


函数的调用方式其实没那么复杂,基本上就是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  

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、函数的栈平衡操作是由被调用函数在返回之前负责清除栈中的参数。


还有很多调用规则,如:thiscall、naked call、pascal等,有兴趣的读者可以自己去研究一下。


参数传递方式


函数参数的传递方式无外乎两种,一种是通过寄存器传递,另一种是通过内存传递。这两种传递方式在我们平时的开发中并不会被关注,因为不在特殊情况下,这两种传递方式,都可以满足要求。但是,我们要写的是操作系统,在操作系统里面有很多苛刻的环境要求,这使得我们不得不了解这些参数传递方式,来解决这些问题。


  • 寄存器传递


寄存器传递就是将函数的参数放到寄存器里传递,而不是放到栈里传递。这样的好处主要是执行速度快,编译后生成的代码量少。但只有少部分调用规定默认是通过寄存器传递参数,大部分编译器是需要特殊指定使用寄存器传递参数的。


在X86体系结构下,系统调用一般会使用寄存器传递,由于作者看过的内核种类有限,也不能确定所有的内核都是这么处理的,但是Linux内核肯定是这么做的。因为应用程序的执行空间和系统内核的执行空间是不一样的,如果想从应用层把参数传递到内核层的话,最方便快捷的方法是通过寄存器传递参数,否则需要使用很大的周折才能把数据传递过去,原因会在以后的章节中详细讲述。


  • 内存传递


内存传递参数很好理解,在大多数情况下参数传递都是通过内存入栈的形式实现的。


在X86体系结构下的Linux内核中,中断或异常的处理会使用内存传递参数。因为,在中断产生后,到中断处理的上半部,中间的过渡代码是用汇编实现的。汇编跳转到C语言的过程中,C语言是用堆栈保存参数的,为了无缝衔接,汇编就需要把参数压入栈中,然后再跳转到C语言实现的中断处理程序中。


以上这些都是在X86体系结构下的参数传递方式,在X64体系结构下,大部分编译器都使用的是寄存器传递参数。因此,内存传递和寄存器传递的区别就不太重要了。



●本文编号58,以后想阅读这篇文章直接输入58即可。

●输入m可以获取到文章目录


推荐15个技术类公众微信

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
c++函数调用过程分析(汇编语言编码是ATT模式 可以手动修改为intel风格依个人喜好我个人更偏向intel风格)
读懂操作系统(x64)之堆栈帧(过程调用)
x86_64架构下的函数调用及栈帧原理
通过一段汇编,加深对寄存器ESP和EBP的理解
自己实现IDispatch::Invoke方法
【新提醒】[教程]逆向反汇编第二课
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服