gcc内联汇编入门

本文详细介绍了GCC内联汇编的使用,从AT&T语法基础开始,涵盖源目操作符顺序、寄存器和立即数、寻址方式以及后缀修饰。接着讨论了C函数的参数传递,包括参数如何通过堆栈传递,frame pointer的作用以及如何清理参数表。最后,通过实例展示了非内联汇编和内联汇编的代码对比,揭示了内联汇编在优化代码和提高效率方面的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在读linux0.10内核,正好遇到了内联汇编。

gcc其中一个很强大的功能就是内联汇编。

但是苦于广大学生(包括我)都被intel语法毒害很深,结果没几个会用gcc内联汇编的了。。。

 

一开始我也看的云里雾里,但是一用gcc -S测试一下,马上就非常明白了。

所以还是实践为王。

这里就稍微从AT&T语法一直介绍到gcc内联汇编运用实例。

 

AT&T语法。

其实AT&T语法真的不难。。。看了20min大概就懂得差不多了。

推荐直接看gnu as的pdf,总共16页,而且涵盖了不少下面要将的内容。

这里只提个概要。

1.源目操作符顺序

同一条语句intel语法 MOV EAX,EBX 源在后,目在前(我自己其实很分不清源和目,只知道咋用),intel这句就是把EBX的值赋给EAX。

AT&T语法就是mov %ebx,%eax,顺序正好相反,其实个人觉得AT&T这个顺序更加直观。

 

2.寄存器,立即数

上面其实就已经有了寄存器的例子了,用寄存器的话要用%表示,否则会被解释成对应名称的内存地址

立即数同理,只不过必须用$前缀,不然表示地址。立即数方面,intel一般都是用h后缀表示16进制,而AT&T用更像C的0x前缀表示。

 

3.寻址

intel语法的变址+基址的时候

[基址寄存器+变址寄存器X比例因子+立即数],eg:[bx+si*4+1],很简单啊。。。

AT&T就比较难懂了

立即数(基址寄存器,变址寄存器,比例因子),eg :$1(%bx,%si,$4)再带上之前说的两个标准,看得确实蛋疼了。。。

 

4.后缀修饰

intel语法使用 word ptr 之类的命令来指明寻址内存时读取数据的长度。虽然vim C-N上下文补全很方便,但是还是蛮蛋疼的。

AT&T直接使用后缀表示,l=long=32bits,w=word=16bits,b=byte=8bits.比起intel语法方便多了

同样,有明确长度时(例如指明了寄存器),就可以省略后缀修饰。

 

其余变化应该不大

 

C函数传递。

这个直接关系到C与汇编的混合编程。

当然,这段写的最好的还是gnu as manual。

这里也主要是几个要点。

1.C函数的参数

C语言是通过堆栈传参的。按照参数表顺序依次将参数压入堆栈,然后call。

 

2.C函数的frame pointer

用gcc编译的时候,如果不用-fomit-frame-pointer选项的话,默认是会开启frame pointer功能。

此时在函数体一开始一般会有

pushl %ebp

mov %esp,%ebp;这两行等价于enter这一条命令

这两句,这就是建立了frame pointer。

此时esp,ebp都指向栈顶,而栈顶是之前的ebp的值。

由于call 调用函数的时候,要压入eip(这里全采用32bit linux为例,linux所有代码处于同一segment然后使用分页机制,所以不存在压入cs的情况)

所以最后一个函数参数就是ebp+8.(4字节被ebp占去,4字节在被之前压入的eip占去)

在ebp+8之前(包括ebp+8。之前指高地址处),就是函数的参数表。

ebp之后(之后指低地址处),就是函数体内的临时变量占用的位置。

 

如果函数内定义了临时变量,在frame pointer构造好了之后,就要subl $X,%esp,X为临时变量占用的字节数。

否则push pop就会轻松的毁了函数体内的临时变量。

 

所以此时想要访问到函数的参数,就可以用8(%ebp)这样寻址。(8(%ebp)就是最后一个参数,16(%ebp)就是倒数第二个,以此类推)

想要访问函数体的局部变量就可以用-4(%ebp)这样的寻址。(-4(%ebp)就是第一个局部变量,-8(%esbp)就是第二个,以此类推)

而堆栈的pop push只影响esp,不影响ebp。

所以ebp就被称为frame pointer。是函数体内部临时变量和函数参数表的分水岭。(其实(%ebp)~8(%ebp)之间的分别是eip和原ebp)

 

退出函数体时,如果函数运行完了,esp正好指在原来的ebp值,直接popl %ebp,因为此时esp就指在原本ebp的值的后面。

如果esp不指在ebp上也很简单:

movl %ebp,%esp

popl %ebp;这两行等价于leave这一条命令

 

如果用了-fomit-frame-pointer的话,进入函数和退出函数的时候,都不用建立frame pointer,直接使用esp+立即数来寻址。

由于esp会随着堆栈变化而变化,所以debug的时候很是绕脑子。。。都绕人脑了,debuger显然是没法识别出了。不过这样就减少了一点点函数调用的开销。

 

之后就可以ret了。

 

3.C函数参数表的清理

既然通过堆栈传参,就必须在函数运行完后,清除堆栈中的参数表(根据上一点,可以发现,函数在ret前销毁了frame pointer,所以堆栈中已经没有函数的临时变量了),而清楚堆栈参数表的工作,是由函数的调用者处理的。

addl 4,%esp

如上就可以清除一个只有一个参数的函数的参数表。

 

看到这里,应该就有内联汇编所需的基础知识了。

 

非内联汇编实例:

用C写了个简单的swap函数:

然后用gcc -S 编译成汇编代码。如下:

看得蛋疼了吧。。。

人家gcc不开任何优化的话,就是忠实的按照你的要求,只把寄存器作为中间介质,对内存勤勤恳恳的操作着。

我们都知道那个蛋疼的swap是干啥用的。于是我们稍微改下swap来直接实现交换。由于我们手写,自然用不着建立frame pointer,能省则省:

 

这下体积大幅精简了。。。

重新编译下,嗯,结果正常。。。但是代码从原来的20+行汇编精简到6行,是不是感觉很爽啊。。。

其实,gcc -S -O2 -fomit-frame-pointer结果如下:

我们可以发现,开了-O2之后,gcc大量使用寄存器,极大的减少了多余的寻址操作。并且用了-fomit-frame-pointer,导致少了4行frame pointer相关的命令,结果总共只用9行就完成了之前20+行的功能。所以-O2还是很强大的,不过再强大还是比手工的多了3行加减法运算。这就是编译器的极限啊。。。。


内联汇编。

有刚才手动修改的汇编实例之后,轮到真正强大内联汇编了。

内联汇编基本命令:

asm ("汇编代码"

    :输出变量

    :输入变量

    :寄存器修改状况)

其中汇编代码必须有(废话),输出输入必须有,空的话就空着。寄存器修改状况可选。

对于输出输入变量的属性,可以参考完整的gnu gcc手册。个人推荐google搜索到的一个gcc-inline-asm.pdf文档。

举例说明比较好:

输出时需要“=”表示输出装载,a表示ax,eax,al这一系列的寄存器,根据长度自动选择。

如果表示装载属性的“”里什么都没有,表示继承前一项的寄存器安排(不集成那个等于号)“m”表示内存。

既然“a”表示a系列寄存器,“b”自然就是b系列了。“r”就是可用寄存器。其余查手册吧。

输出变量的第一个变量在内联汇编中可以通过%0引用,然后依次为%1,%2一直延续到输入变量

在内联汇编中出现的寄存器,都用%%eax这种形式表示。

 

可能输出输入这种叫法很不好理解,我更倾向与装入这种叫法。

其中输入变量如果是寄存器属性的,是在执行汇编命令前,由gcc负责将数据装入寄存器。内存属性则不作任何变化,直接使用ebp+偏移或者esp+偏移的形式出现在代码中(取决于是否建立frame pointer)。

输出变量如果是寄存器属性的,则在汇编指令执行后,由gcc负责将数据从寄存器送回到原本的地址上。

对于内存属性的,由于本身就在内存上,所以不需要也不存在内存属性的输出变量。

 

有了上述讲解,再做一个宏+内联汇编的例子:

gcc -S的结果,(-O2不-O2无所谓了。。。)

 

其中#APP开头到#NO_APP部分,就是内联汇编的代码,

    movl    28(%esp), %eax这一行就是gcc做的输入变量的装入。

    movl    %eax, 28(%esp)这一行就是gcc做的输出变量的回送。

由于做成了宏,不需要通过指针传参,代码再度精简。

 

于是,下次笔试遇到啥用C宏写个交换两个数的值的时候,就写个内联汇编吧。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值