作者 | 刀哥编辑| 欧阳淑丽
在Linux代码中,经常可以看到部分汇编代码嵌入在C代码中,这些代码要么与硬件系统相关,要么对性能有关键影响。
很早以前,我就非常害怕内联汇编代码,直到后来汇编部分的弱点被弥补了,才彻底结束了这种心理。
也许你在工作中很少涉及内联汇编代码,但是一旦踏入系统底层,或者需要优化时间紧迫的场景,此时你的知识储备就会发挥重要作用!
这篇文章我们来详细讲一下如何在C语言中通过asm关键字嵌入汇编语言代码。 文章中的8个示例代码从简单到复杂,逐步介绍内联汇编的关键语句规则。 。
希望这篇文章能够成为你成为大神路上的垫脚石!
附:
示例代码在Linux系统下使用AT&T汇编句型;
文章中的8个示例代码,可以在公众号后台回复【426】获取下载地址;
基本asm格式
gcc 编译器支持 2 种内联 asm 代码的方法:
基本asm格式:不支持操作数;
扩展asm格式:支持操作数;
1.1 语法规则
asm [易失性](“汇编指令”)
所有指令必须用双引号引起来;
多条指令必须用n分隔符分隔,一般加t用于排版;
多条汇编指令可以一行或多行编写;
关键字asm可以替换为asm;
易失性是可选的,编译器可以优化汇编代码。 使用 volatile 关键字后,告诉编译器不要优化手写的内联汇编代码。
1.2 test1.c插入空指令
#include
int main()
{
asm ("nop");
printf("hellon");
asm ("nopntnopnt"
"nop");
return 0;
}
注意:在C语言中,两个连续的字符串字面量会被手动拼接成一个字符串,因此两个字符串“nopntnopnt”“nop”会被手动拼接成一个字符串。
生成汇编代码指令:
gcc -m32 -S -o test1.s test1.c
test1.s的内容如下(仅粘贴内联汇编代码的相关部分):
#APP
# 5 "test1.c" 1
nop
# 0 "" 2
#NO_APP
// 这里是 printf 语句生成的代码。
#APP
# 7 "test1.c" 1
nop
nop
nop
# 0 "" 2
#NO_APP
正如您所看到的,内联汇编代码由两个注释(#APP ... #NO_APP)包裹。 源代码中嵌入了两段汇编代码,因此可以看到gcc编译器生成的汇编代码包含了这两部分代码。
这两部分嵌入的汇编代码是空指令nop,没有任何意义。
1.3 test2.c操作全局变量
在C代码中嵌入汇编指令的目的是使用它们进行估计或执行某些功能。 我们来看看如何在内联汇编指令中操作全局变量。
#include
int a = 1;
int b = 2;
int c;
int main()
{
asm volatile ("movl a, %eaxnt"
"addl b, %eaxnt"
"movl %eax, c");
printf("c = %d n", c);
return 0;
}
汇编指令中有关编译器的基本知识:
eax和ebx都是x86平台上的寄存器(32位)。 在基本的asm格式中,寄存器后面必须添加百分号%。
32位寄存器eax可以用作16位(ax),或用作8位(ah,al)。 本文将仅使用 32 位。
代码说明:
movl a, %eax // 将变量 a 的值复制到 %eax 寄存器;
addl b, %eax // 将变量b的值与%eax寄存器中的值(a)相加,并将结果放入%eax寄存器中;
movl %eax, c // 将%eax寄存器中的值复制到变量c;
生成汇编代码指令:
gcc -m32 -S -o test2.s test2.c
test2.s的内容如下(只贴出内联汇编代码的相关部分):
#APP
# 9 "test2.c" 1
movl a, %eax
addl b, %eax
movl %eax, c
# 0 "" 2
#NO_APP
可以看到,在内联汇编代码中,可以直接使用全局变量a和b的名称来进行操作。 执行test2,可以得到正确的结果。
思考一个问题:为什么汇编代码中可以使用变量a、b、c?
查看test2.s中内联汇编代码之前的部分,可以看到:
.file "test2.c"
.globl a
.data
.align 4
.type a, @object
.size a, 4
a:
.long 1
.globl b
.align 4
.type b, @object
.size b, 4
b:
.long 2
.comm c,4,4
变量a和b用.globl修饰,c用.comm修饰,相当于将它们导入为全局的,所以可以在汇编代码中使用。
那么问题来了:如果是局部变量,在汇编代码中是不会用.globl导出的。 这时候是不是可以直接在内联汇编指令中使用呢?
眼见为实,让我们将这 3 个变量放入 main 函数中,并尝试将它们作为局部变量。
1.4 test3.c尝试操作局部变量
#include
int main()
{
int a = 1;
int b = 2;
int c;
asm("movl a, %eaxnt"
"addl b, %eaxnt"
"movl %eax, c");
printf("c = %d n", c);
return 0;
}
生成汇编代码指令:
gcc -m32 -S -o test3.s test3.c
在test3.s中,可以看到a、b、c没有导入符号,并且a、b在其他地方也没有使用,所以它们的值直接复制到了栈空间:
movl $1, -20(%ebp)
movl $2, -16(%ebp)
我们尝试将其编译成可执行程序:
$ gcc -m32 -o test3 test3.c
/tmp/ccuY0TOB.o: In function `main':
test3.c:(.text+0x20): undefined reference to `a'
test3.c:(.text+0x26): undefined reference to `b'
test3.c:(.text+0x2b): undefined reference to `c'
collect2: error: ld returned 1 exit status
编译错误:找不到a、b、c的引用! 那么使用局部变量应该怎么做呢? 扩展asm格式!
扩展asm格式
2.1 指令格式
asm [易失性](“汇编指令”:“输出操作数列表”:“输入操作数列表”:“更改的寄存器”)
格式说明
汇编指令:与基本asm格式相同;
输出操作数列表:汇编代码如何将处理结果传送给C代码;
输入操作数列表:C代码如何将数据传递给内联汇编代码;
更改寄存器:告诉编译器我们在内联汇编代码中使用了哪些寄存器;
“Modified register”可以省略,最后一个逗号也可以省略,但上面的逗号必须保留,即使输出/输入操作数列表为空。
让我解释一下“改变的寄存器”:gcc在编译C代码时需要使用一系列寄存器; 我们手写的内联汇编代码中也使用了一些寄存器。
为了通知编译器,让它知道:我们的用户在内联汇编代码中使用了哪些寄存器,你可以在这里列出它们,在这种情况下,gcc将阻止使用此类列出的寄存器
2.2 输出和输入操作数列表的格式
系统中,存储变量的地方只有两个:寄存器和显存。 因此,告诉内联汇编代码输出和输入操作数实际上是告诉它:
将结果输出到哪个寄存器或内存地址;
从哪个寄存器或内存地址读取输入数据;
这个过程还必须满足一定的格式:
"[输出修饰符]约束"(寄存器或内存地址)
(一)约束条件
就是通过不同的字符告诉编译器要使用什么寄存器或者内存地址。 包括以下字符:
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个修饰符:
+:修改的操作数可读写;
=:修改的操作数只能写;
%:修改后的操作数可以与下一个操作数互换;
&:在内联函数完成之前,修改的操作数可以删除或重复使用;
语言描述比较具体,直接看例子吧!
2.3 test4.c 通过寄存器操作局部变量
#include
int main()
{
int data1 = 1;
int data2 = 2;
int data3;
asm("movl %%ebx, %%eaxnt"
"addl %%ecx, %%eax"
: "=a"(data3)
: "b"(data1),"c"(data2));
printf("data3 = %d n", data3);
return 0;
}
有2个地方需要注意:
在内联汇编代码中,“改变的寄存器”列表没有声明,也就是说可以省略(前面的逗号不需要);
扩展asm格式中,寄存器后面必须写2%;
代码解释:
"b"(data1), "c"(data2) ==> 将变量 data1 复制到寄存器 %ebx,将变量 data2 复制到寄存器 %ecx。 这样在内联汇编代码中就可以通过这两个寄存器来操作这两个数字;
"=a"(data3) ==> 将处理结果放入寄存器%eax中简单网站 源码,然后复制到变量data3中。 前面的修饰符等号表示:会将数据写入%eax,但不会从中读取数据;
通过前面的格式,在内联汇编代码中,可以使用指定的寄存器来操作局部变量。 稍后我们将看到局部变量是如何从堆栈空间复制到寄存器的。
生成汇编代码指令:
gcc -m32 -S -o test4.s test4.c
汇编代码test4.s如下:
movl $1, -20(%ebp)
movl $2, -16(%ebp)
movl -20(%ebp), %eax
movl -16(%ebp), %edx
movl %eax, %ebx
movl %edx, %ecx
#APP
# 10 "test4.c" 1
movl %ebx, %eax
addl %ecx, %eax
# 0 "" 2
#NO_APP
movl %eax, -12(%ebp)
正如您所看到的,在进入手写的内联汇编代码之前:
将数字1通过栈空间(-20(%ebp))复制到寄存器%eax,然后再复制到寄存器%ebx;
将数字2通过堆栈空间(-16(%ebp))复制到寄存器%edx,然后再复制到寄存器%ecx;
这两个操作对应于内联汇编代码中的“输入操作数列表”部分:“b”(data1)、“c”(data2)。
然后内联汇编代码(在#NO_APP之后)将%eax寄存器中的值复制到堆栈中的-12(%ebp)位置,即局部变量data3所在的位置,从而完成输出操作。
2.4 test5.c声明改变的寄存器
在test4.c中,我们没有声明更改的寄存器,因此编译器可以自由选择使用哪些寄存器。 从生成的汇编代码test4.s可以看出,gcc使用了%edx寄存器。
那么让我们测试一下:告诉 gcc 不要使用 %edx 寄存器。
#include
int main()
{
int data1 = 1;
int data2 = 2;
int data3;
asm("movl %%ebx, %%eaxnt"
"addl %%ecx, %%eax"
: "=a"(data3)
: "b"(data1),"c"(data2)
: "%edx");
printf("data3 = %d n", data3);
return 0;
}
代码中,asm指令的最后一部分“%edx”用于告诉gcc编译器:在内联汇编代码中,我们将使用%edx寄存器,因此不需要使用它。
生成汇编代码指令:
gcc -m32 -S -o test5.s test5.c
看一下生成的汇编代码test5.s:
movl $1, -20(%ebp)
movl $2, -16(%ebp)
movl -20(%ebp), %eax
movl -16(%ebp), %ecx
movl %eax, %ebx
#APP
# 10 "test5.c" 1
movl %ebx, %eax
addl %ecx, %eax
# 0 "" 2
#NO_APP
movl %eax, -12(%ebp)
可以看出,在内联汇编代码之前,gcc并没有选择使用寄存器%edx。
使用占位符替换寄存器名称
在前面的例子中,只使用了2个寄存器来操作2个局部变量。 如果操作数很多,在内联汇编代码中写每个寄存器的名字就变得很不方便。
因此,扩展的 asm 格式为我们提供了另一种在输出和输入操作数列表中使用寄存器的惰性方法:占位符!
占位符有点类似于批处理脚本,用2...来引用输入参数,内联汇编代码中的占位符,从输出操作数列表中的寄存器开始,从0开始编号到输入操作的所有寄存器在列表中。
看案例更直观!
3.1 test6.c使用占位符代替寄存器
#include
int main()
{
int data1 = 1;
int data2 = 2;
int data3;
asm("addl %1, %2nt"
"movl %2, %0"
: "=r"(data3)
: "r"(data1),"r"(data2));
printf("data3 = %d n", data3);
return 0;
}
代码说明:
输出操作数列表“=r”(data3):约束使用字符r,也就是说不指定寄存器,编译器选择使用哪个寄存器来存储结果,最后复制到局部变量data3 ;
输入操作数列表“r”(data1)、“r”(data2):约束字符r,不指定寄存器,编译器选择使用哪2个寄存器来接收局部变量data1和data2;
输出操作数列表中只需要一个寄存器,因此内联汇编代码中的%0代表该寄存器(即:从0开始计数);
输入操作数列表中有2个寄存器,因此内联汇编代码中的%1和%2代表这2个寄存器(即:从输出操作数列表的最后一个寄存器开始顺序计数);
生成汇编代码指令:
gcc -m32 -S -o test6.s test6.c
test6.s的汇编代码如下:
movl $1, -20(%ebp)
movl $2, -16(%ebp)
movl -20(%ebp), %eax
movl -16(%ebp), %edx
#APP
# 10 "test6.c" 1
addl %eax, %edx
movl %edx, %eax
# 0 "" 2
#NO_APP
movl %eax, -12(%ebp)
可以看到,gcc编译器选择%eax存储局部变量data1简单网站 源码,%edx存储局部变量data2,然后运算结果也存储在%eax寄存器中。
您觉得这样操作是不是更方便呢? 我们不需要指定使用哪些寄存器,只需让编译器选择即可。
在内联汇编代码中,寄存器使用%0、%1、%2 等占位符来使用。
别担心,如果你觉得使用数字仍然麻烦且容易出错,还有另一种更方便的操作:扩展的asm格式还允许重命名那些占位符,即给每个寄存器一个别名,然后内联汇编代码使用别名来操作寄存器。
还是看代码吧!
3.2test7.c给寄存器起别名
#include
int main()
{
int data1 = 1;
int data2 = 2;
int data3;
asm("addl %[v1], %[v2]nt"
"movl %[v2], %[v3]"
: [v3]"=r"(data3)
: [v1]"r"(data1),[v2]"r"(data2));
printf("data3 = %d n", data3);
return 0;
}
代码说明:
输出操作数列表:给寄存器(由gcc编译器选择)一个别名v3;
输入操作数列表:寄存器(由gcc编译器选择)别名为v1和v2;
设置好别名后,就可以在内联汇编代码中直接使用这个别名(%[v1]、%[v2]、%[v3])来操作数据。
生成汇编代码指令:
gcc -m32 -S -o test7.s test7.c
我们看一下生成的汇编代码test7.s:
movl $1, -20(%ebp)
movl $2, -16(%ebp)
movl -20(%ebp), %eax
movl -16(%ebp), %edx
#APP
# 10 "test7.c" 1
addl %eax, %edx
movl %edx, %eax
# 0 "" 2
#NO_APP
movl %eax, -12(%ebp)
这部分的汇编代码与test6.s中的完全一样!
使用视频内存位置
在上面的例子中,输出操作数列表和输入操作数列表都使用寄存器(约束字符:a、b、c、d、r等)。
我们可以指定使用哪个寄存器,也可以让编译器选择使用哪个寄存器。 通过寄存器操作数据会更快。
如果我们愿意的话,我们也可以直接使用变量的内存地址来操作变量,这时候我们就需要使用约束字符m。
4.1 test8.c使用内存地址来操作数据
#include
int main()
{
int data1 = 1;
int data2 = 2;
int data3;
asm("movl %1, %%eaxnt"
"addl %2, %%eaxnt"
"movl %%eax, %0"
: "=m"(data3)
: "m"(data1),"m"(data2));
printf("data3 = %d n", data3);
return 0;
}
代码说明:
输出操作数列表“=m”(data3):直接使用变量data3的内存地址;
输入操作数列表“m”(data1)、“m”(data2):直接使用变量data1、data2的内存地址;
在内联汇编代码中,因为需要进行减法计算,所以需要用到寄存器(%eax),而这个计算肯定需要寄存器。
当操作这些内存地址中的数据时,仍然使用按顺序编号的占位符。
生成汇编代码指令:
gcc -m32 -S -o test8.s test8.c
生成的汇编代码如下test8.s:
movl $1, -24(%ebp)
movl $2, -20(%ebp)
#APP
# 10 "test8.c" 1
movl -24(%ebp), %eax
addl -20(%ebp), %eax
movl %eax, -16(%ebp)
# 0 "" 2
#NO_APP
movl -16(%ebp), %eax
可以看到,在进入内联汇编代码之前,先将data1和data2的值放入栈中,然后直接与寄存器%eax对栈中的数据进行操作,最后得到操作结果(% eax)被复制到栈上data3的位置(-16(%ebp))。
总结
通过上面的8个例子,我们已经解释了内联汇编代码中的关键句型规则。 有了这个基础,我们就可以在内联汇编代码中编译出更复杂的指令了。
希望以上内容能够对您有所帮助! 谢谢!
60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!
直接扫码或微信搜索「CSDN」公众号,后台回复关键词「路线图」,即可获取完整路线图!
☞百度首批无人网约车开始运营;美团饿了么正式回应:已逐步取消对骑士逐单处罚;Qt 2021 路线图公布|极客头条
☞程序员薪资两极分化,如何成为高薪程序员?
☞JavaScript 开发者数量暴涨、C# 超越 PHP,揭晓全球开发最新趋势!
发表评论