当前位置: 首页 > 科技观察

内联汇编可怕吗?看完这篇文章,结束吧!

时间:2023-03-16 01:39:54 科技观察

在Linux代码中,经常可以看到C代码中嵌入了一些汇编代码,这些代码要么与硬件系统有关,要么对性能有关键影响。很久以前,我特别害怕内联汇编代码,直到我把汇编部分的不足补上,才彻底结束了这种心态。也许你在工作中几乎不涉及内联汇编代码,但是一旦进入系统底层,或者需要优化时间紧迫的场景,这时候你的知识储备就会发挥重要作用!这篇文章,我们就来详细谈谈如何在C语言中通过asm关键字嵌入汇编语言代码。本文8个示例代码由浅入深,逐步介绍内联汇编的关键语法规则。希望这篇文章能成为你成为高手的敲门砖!PS:示例代码使用Linux系统下的AT&T汇编语法;文章中的8个示例代码,可以在公众号后台回复【426】,即可获得下载地址;1.基本asm格式gcc编译器支持2种形式的内联asm代码:基本asm格式:不支持操作数;扩展asm格式:支持操作数;1.语法规则asm[volatile]("汇编指令")所有指令必须用双引号括起来;多条指令必须用\n分隔符隔开,排版一般加\t;多条汇编指令可以写在一行中,也可以写在多行中;关键字asm可以替换为asm;volatile是可选的,编译器可能会优化汇编代码,使用volatile关键字后,告诉编译器不要优化手写的内联汇编代码。2.Test1.c插入一条空指令#includeintmain(){asm("nop");printf("hello\n");asm("nop\n\tnop\n\t""nop");return0;}注:C语言会自动将两个连续的字符串字面量拼接成一个,所以"nop\n\tnop\n\t""nop"这两个字符串会自动拼接成一个字符串。生成汇编代码指令:gcc-m32-S-otest1.stest1.ctest1.s内容如下(只贴出内联汇编代码的相关部分):#APP#5"test1.c"1nop#0""2#NO_APP//这里是printf语句生成的代码。#APP#7"test1.c"1nopnopnop#0""2#NO_APP可以看到内联汇编代码被两条注释(#APP...#NO_APP)包裹起来。源代码中嵌入了两段汇编代码,所以可以看到gcc编译器生成的汇编代码包含了这两部分代码。这两部分嵌入式汇编代码都是空指令nop,没有意义。3、test2.c操作全局变量,在C代码中嵌入汇编指令,用于计算或执行某些功能。我们来看看如何在内联汇编指令中操作全局变量。#includeinta=1;intb=2;intc;intmain(){asmvolatile("movla,%eax\n\t""addlb,%eax\n\t""movl%eax,c");printf("c=%d\n",c);return0;}汇编指令中编译器的基本知识:eax,ebx是x86平台下的寄存器(32位),在基本的asm格式中,寄存器必须以百分号%开头。32位寄存器eax可以用作16位(ax),也可以用作8位(ah,al)。本文将只使用32位。代码说明:movla,%eax //复制变量a的值到%eax寄存器;addlb,%eax//将变量b的值与%eax寄存器中的值(a)相加,结果放入%eax寄存器;movl%eax,c//将%eax寄存器中的值复制到变量c中;生成汇编代码指令:gcc-m32-S-otest2.stest2.ctest2.s内容如下(只贴内联汇编代码相关部分):#APP#9"test2.c"1movla,%eaxaddlb,%eaxmovl%eax,c#0""2#NO_APP可以看到,在内联汇编代码中,可以直接使用全局变量a、b的名字进行操作。执行test2,可以得到正确的结果。想一个问题:为什么汇编代码中可以使用变量a、b、c?查看test2.s中内联汇编代码之前的部分,可以看到:.file"test2.c".globla.data.align4。typea,@object.sizea,4a:.long1.globlb.align4.typeb,@object.sizeb,4b:.long2.commc,4,4个变量a,b被.globl修饰,c被.comm修饰,这相当于全局导出它们,以便它们可以在汇编代码中使用。那么问题来了:如果是局部变量,在汇编代码中是不会用.globl导出的。这时候是不是可以直接在内联汇编指令中使用呢?眼见为实,我们把这3个变量放在Go的main函数里面,作为局部变量试试看。4.test3.c尝试操作局部变量#includeintmain(){inta=1;intb=2;intc;asm("movla,%eax\n\t""addlb,%eax\n\t""movl%eax,c");printf("c=%d\n",c);return0;}生成汇编代码指令:gcc-m32-S-otest3.stest3.c可以在test3中使用.s看到a,b,c没有exportedsymbol,a,b没有在别处使用,所以直接把它们的值复制到栈空间:movl$1,-20(%ebp)movl$2,-16(%ebp)让我们尝试将其编译成可执行程序:$gcc-m32-otest3test3.c/tmp/ccuY0TOB.o:Infunction`main':test3.c:(.text+0x20):undefinedreferenceto`a'test3.c:(.text+0x26):undefinedreferenceto`b'test3.c:(.text+0x2b):undefinedreferenceto`c'collect2:error:ldreturned1exitstatus编译错误:找不到对a、b、c的引用!那怎么办,局部变量可以用吗?扩展asm格式!二、扩展asm格式1、指令格式asm[volatile]("汇编指令":"输出操作数列表":"输入操作数列表":"修饰寄存器")格式汇编指令说明:同基本asm格式;输出操作数列表:汇编代码如何将处理结果传递给C代码;输入操作数列表:C代码如何将数据传输到内联汇编代码;changedregisters:告诉编译器Registers,我们在内联汇编代码中使用了哪些寄存器;“changedregisters”可以省略,此时最后一个冒号可以省略,但前面的冒号必须保留,即使输出/输入操作数列表为空。我解释一下“改变的寄存器”:gcc在编译C代码时需要用到一系列的寄存器;我们手写的内联汇编代码中也使用了一些寄存器。为了通知编译器,让它知道:我们的用户在内联汇编代码中使用了哪些寄存器,可以在这里列出来,这样gcc会避免使用这些列出的寄存器2.输出和输入操作数列表的格式在系统中,存放变量的地方只有两个:寄存器和内存。因此,告诉内联汇编代码输出和输入操作数,实际上就是告诉它:将结果输出到哪些寄存器或内存地址;从哪些寄存器或内存地址读取输入数据;这个过程也满足一定的格式:“[输出修饰符]约束”(寄存器或内存地址)(1)约束是通过不同的字符告诉编译器使用哪些寄存器或内存地址。包括以下字符:a:使用eax/ax/al寄存器;b:使用ebx/bx/bl寄存器;c:使用ecx/cx/cl寄存器;d:使用edx/dx/dl寄存器;r:使用任何可用的通用寄存器;m:变量的内存位置;先记住这些就够了,其他约束选项包括:D、S、q、A、f、t、u等,需要的时候查看文档。(2)输出修饰符顾名思义,就是用来修饰输出的,为输出寄存器或内存地址提供额外的指令,包括以下4个修饰符:+:被修饰的操作数可读可写;=:修改后的操作数只能写;%:修改后的操作数可以与下一个操作数互换;&:在内联函数完成之前,修改后的操作数可以删除或重用;语言描述比较抽象,直接看例子!3、test4.c通过寄存器操作局部变量#includeintmain(){intdata1=1;intdata2=2;intdata3;asm("movl%%ebx,%%eax\n\t""addl%%ecx,%%eax":"=a"(data3):"b"(data1),"c"(data2));printf("data3=%d\n",data3);return0;}有两个地方需要注意:内联汇编代码中,没有“改变的寄存器”列表,也就是说可以省略(前面的冒号不需要);在扩展asm格式中,必须写入2%之前的寄存器;代码解释:"b"(data1),"c"(data2)==>复制变量data1到寄存器%ebx,变量data2复制到寄存器%ecx。这样,在内联汇编代码中,就可以通过这两个寄存器来操作两个数;"=a"(data3)==>将处理结果放入寄存器%eax中,然后复制到变量data3中。前面的修饰符等号表示:将数据写入%eax,不会从中读取数据;通过上面的格式,在内联汇编代码中,就可以使用指定的寄存器来操作局部变量了,后面我们会看到局部变量是如何从栈空间复制到寄存器中的。生成汇编代码指令:gcc-m32-S-otest4.stest4.c汇编代码test4.s如下:movl$1,-20(%ebp)movl$2,-16(%ebp)movl-20(%ebp),%eaxmovl-16(%ebp),%edxmovl%eax,%ebxmovl%edx,%ecx#APP#10"test4.c"1movl%ebx,%eaxaddl%ecx,%eax#0""2#NO_APPmovl%eax,-12(%ebp)可以看出,在进入手写内联汇编代码之前:将数字1通过栈空间(-20(%ebp))复制到寄存器%eax中,然后再复制到寄存器中%ebx;把数2通过栈空间(-16(%ebp)),复制到寄存器%edx,再复制到寄存器%ecx;这两个操作对应于内联汇编代码中的“输入操作数列表”部分:“b”(data1)、“c”(data2)。内联汇编代码之后(#NO_APP之后),将%eax寄存器中的值复制到栈中的-12(%ebp)位置,也就是局部变量data3所在的位置,从而完成输出操作。4.test5.c声明改变的寄存器在test4.c中,我们没有声明改变的寄存器,所以编译器可以任意选择使用哪些寄存器。从生成的汇编代码test4.s可以看出,gcc使用了%edx寄存器。所以让我们测试一下:告诉gcc不要使用%edx寄存器。#includeintmain(){intdata1=1;intdata2=2;intdata3;asm("movl%%ebx,%%eax\n\t""addl%%ecx,%%eax":"=a"(data3):"b"(data1),"c"(data2):"%edx");printf("data3=%d\n",data3);return0;}代码中,最后一部分asm指令的“%edx”用于告诉gcc编译器:在内联汇编代码中,我们将使用%edx寄存器,你不应该使用它。生成汇编代码指令:gcc-m32-S-otest5.stest5.c看一下生成的汇编代码test5.s:movl$1,-20(%ebp)movl$2,-16(%ebp)movl-20(%ebp),%eaxmovl-16(%ebp),%ecxmovl%eax,%ebx#APP#10"test5.c"1movl%ebx,%eaxaddl%ecx,%eax#0""2#NO_APPmovl%eax,-12(%ebp)可以看出,在内联汇编代码之前,gcc并没有选择使用寄存器%edx。3、用占位符代替寄存器名上面的例子中只用了2个寄存器来操作2个局部变量。如果操作数较多,则在内联汇编代码中写出每个寄存器的名称。看起来很不方便。因此,扩展的asm格式为我们提供了另一种在输出和输入操作数列表中使用寄存器的懒惰方式:占位符!占位符有点类似于批处理脚本,使用2...来引用和输入参数一样,内联汇编代码中的占位符从0开始编号,从输出操作数列表中的寄存器开始,到输入操作数列表中的所有寄存器。看例子更直观!1.test6.c使用占位符代替寄存器#includeintmain(){intdata1=1;intdata2=2;intdata3;asm("addl%1,%2\n\t""movl%2,%0":"=r"(data3):"r"(data1),"r"(data2));printf("data3=%d\n",data3);return0;}代码说明:输出操作数列表“=r”(data3):约束使用字符r,也就是说没有指定寄存器,由编译器选择使用哪个寄存器存放结果,最后拷贝到局部变量data3;inputoperationNumberlist"r"(data1),"r"(data2):约束字符r,不指定寄存器,由编译器选择使用哪2个寄存器来接收局部变量data1和data2;输出操作数列表寄存器只需要一个,所以内联汇编代码中的%0代表这个寄存器(即:从0开始计数);输入操作数列表中有2个寄存器,所以内联汇编代码中的%1和%2代表这两个寄存器(即输出操作数列表中从最后一个寄存器开始依次计数);生成汇编代码指令:gcc-m32-S-otest6.stest6.c汇编代码如下test6.s:movl$1,-20(%ebp)movl$2,-16(%ebp)movl-20(%ebp),%eaxmovl-16(%ebp),%edx#APP#10"test6.c"1addl%eax,%edxmovl%edx,%eax#0""2#NO_APPmovl%eax,-12(%ebp)可以可以看到gcc编译器选择%eax存放局部变量data1,%edx存放局部变量data2,然后运算结果也存放在%eax寄存器中。是不是觉得这个操作方便多了?我们不需要指定使用哪些寄存器,交给编译器直接选择。在内联汇编代码中,寄存器使用占位符,如%0、%1、%2。别着急,如果你觉得用数字还是很麻烦,容易出错,还有一个更方便的操作:扩展的asm格式还允许对这些占位符进行重命名,即给每个寄存器起一个别名,然后内联汇编代码使用别名来操作寄存器。还是看代码吧!2.Test7.c给寄存器起别名#includeintmain(){intdata1=1;intdata2=2;intdata3;asm("addl%[v1],%[v2]\n\t""movl%[v2],%[v3]":[v3]"=r"(data3):[v1]"r"(data1),[v2]"r"(data2));printf("data3=%d\n",data3);return0;}代码说明:输出操作数列表:给寄存器一个别名v3(由gcc编译器选择);输入操作数列表:到寄存器(由gcc编译器选择))取一个别名v1和v2;设置别名后,这些别名(%[v1]、%[v2]、%[v3])可以直接在内联汇编代码中使用来操作数据。生成汇编代码指令:gcc-m32-S-otest7.stest7.c我们来看看生成的汇编代码test7.s:movl$1,-20(%ebp)movl$2,-16(%ebp)movl-20(%ebp),%eaxmovl-16(%ebp),%edx#APP#10"test7.c"1addl%eax,%edxmovl%edx,%eax#0""2#NO_APPmovl%eax,-12(%ebp)这部分的汇编代码和test6.s中的完全一样!4、使用内存位置在上面的例子中,输出操作数列表和输入操作数列表都使用了寄存器(约束字符:a、b、c、d、r等)。我们可以指定使用哪个寄存器,也可以让编译器选择使用哪个寄存器。通过寄存器操作数据会更快。如果需要,我们也可以直接使用变量的内存地址来操作变量,此时需要使用约束字符m。1.test8.c使用内存地址操作数据#includeintmain(){intdata1=1;intdata2=2;intdata3;asm("movl%1,%%eax\n\t""addl%2,%%eax\n\t""movl%%eax,%0":"=m"(data3):"m"(data1),"m"(data2));printf("data3=%d\n",数据3);return0;}代码说明:输出操作数列表"=m"(data3):直接使用变量data3的内存地址;输入操作数列表"m"(data1),"m"(data2):直接使用变量data1和data2的内存地址;在内联汇编代码中,因为需要加法计算,所以需要一个寄存器(%eax),这个计算肯定需要寄存器。当操作这些内存地址中的数据时,仍然使用顺序编号的占位符。生成汇编代码指令:gcc-m32-S-otest8.stest8.c生成汇编代码如下test8.s:movl$1,-24(%ebp)movl$2,-20(%ebp)#APP#10"test8。c"1movl-24(%ebp),%eaxaddl-20(%ebp),%eaxmovl%eax,-16(%ebp)#0""2#NO_APPmovl-16(%ebp),%eax可以看到:之前进入内联汇编代码,将data1和data2的值放入栈中,然后直接用寄存器%eax对栈中的数据进行操作,最后将操作结果(%eax)复制到栈中的data3中(-16(%ebp))。五、小结通过以上8个例子,我们已经说明了内联汇编代码中的关键语法规则。有了这个基础,我们就可以在内联汇编代码中编写更复杂的指令了。