网站建设的展望 视频,c 网站设计,手机微网站二级菜单怎么做,盗号网站怎么做在 Linux 代码中#xff0c;经常可以看到在 C 代码中#xff0c;嵌入部分汇编代码#xff0c;这些代码要么是与硬件体系相关的#xff0c;要么是对性能有关键影响的。
在很久以前#xff0c;我特别惧怕内嵌汇编代码#xff0c;直到后来把汇编部分的短板补上之后#xf…在 Linux 代码中经常可以看到在 C 代码中嵌入部分汇编代码这些代码要么是与硬件体系相关的要么是对性能有关键影响的。
在很久以前我特别惧怕内嵌汇编代码直到后来把汇编部分的短板补上之后才彻底终结这种心理。
也许你在工作中几乎不会涉及到内嵌汇编代码的工作但是一旦进入到系统的底层或者需要对时间关键场景进行优化这个时候你的知识储备就发挥重要作用了
这篇文章我们就来详细聊一聊在 C 语言中如何通过 asm 关键字来嵌入汇编语言代码文中的 8 个示例代码从简单到复杂逐步深入地介绍内联汇编的关键语法规则。
希望这篇文章能够成为你进阶高手路上的垫脚石
PS: 示例代码中使用的是 Linux 系统中 ATT 汇编语法 文章中的 8 个示例代码可以在公众号后台回复关键字【 内联汇编示范代码 】即可收到下载地址 一、基本 asm 格式
gcc 编译器支持 2 种形式的内联 asm 代码 基本 asm 格式不支持操作数 扩展 asm 格式支持操作数 1. 语法规则
asm [volatile] (汇编指令)所有指令必须用双引号包裹起来 超过一条指令必须用\n分隔符进行分割为了排版一般会加上\t 多条汇编指令可以写在一行也可以写在多行 关键字 asm 可以使用 asm 来替换 volatile 是可选的编译器有可能对汇编代码进行优化使用 volatile 关键字之后告诉编译器不要优化手写的内联汇编代码。 2. test1.c 插入空指令
#include stdio.h
int main()
{asm (nop);printf(hello\n);asm (nop\n\tnop\n\tnop);return 0;
}注意C语言中会自动把两个连续的字符串字面量拼接成一个所以nop\n\tnop\n\t nop这两个字符串会自动拼接成一个字符串。
生成汇编代码指令
gcc -m32 -S -o test1.s test1.ctest1.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 编译器生成的汇编代码中包含了这两部分代码。
这 2 部分嵌入的汇编代码都是空指令 nop没有什么意义。
3. test2.c 操作全局变量
在 C 代码中嵌入汇编指令目的是用来计算或者执行一定的功能下面我们就来看一下如何在内联汇编指令中操作全局变量。
#include stdio.hint a 1;
int b 2;
int c;int main()
{asm volatile (movl a, %eax\n\taddl b, %eax\n\tmovl %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 // 把变量的值复制到 %eax 寄存器中 addl b, %eax // 把变量 b 的值 与 %eax 寄存器中的值(a)相加结果放在 %eax 寄存器中 movl %eax, c // 把 %eax 寄存器中的值复制到变量 c 中 img
生成汇编代码指令
gcc -m32 -S -o test2.s test2.ctest2.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 中内联汇编代码之前的部分可以看到
.filetest2.c
.globla
.data
.align 4
.typea, object
.sizea, 4
a:
.long1
.globlb
.align 4
.typeb, object
.sizeb, 4
b:
.long2
.commc,4,4变量 a, b 被 .globl 修饰c 被 .comm 修饰相当于是把它们导出为全局的所以可以在汇编代码中使用。
那么问题来了如果是一个局部变量在汇编代代码中就不会用 .globl 导出此时在内联汇编指令中还可以直接使用吗
眼见为实我们把这 3 个变量放到 main 函数的内部作为局部变量来试一下。
4. test3.c 尝试操作局部变量
#include stdio.h
int main()
{ int a 1; int b 2; int c; asm(movl a, %eax\n\t addl b, %eax\n\t 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)img
我们来尝试编译成可执行程序
$ gcc -m32 -o test3 test3.c/tmp/ccuY0TOB.o: In function main:test3.c:(.text0x20): undefined reference to atest3.c:(.text0x26): undefined reference to btest3.c:(.text0x2b): undefined reference to ccollect2: error: ld returned 1 exit status编译报错找不到对 a,b,c 的引用那该怎么办才能使用局部变量呢扩展 asm 格式
二、扩展 asm 格式
1. 指令格式
asm [volatile] (汇编指令 : 输出操作数列表 : 输入操作数列表 : 改动的寄存器)
格式说明 汇编指令与基本asm格式相同 输出操作数列表汇编代码如何把处理结果传递到 C 代码中 输入操作数列表C 代码如何把数据传递给内联汇编代码; 改动的寄存器告诉编译器在内联汇编代码中我们使用了哪些寄存器 “改动的寄存器”可以省略此时最后一个冒号可以不要但是前面的冒号必须保留即使输出/输入操作数列表为空。 关于“改动的寄存器”再解释一下gcc 在编译 C 代码的时候需要使用一系列寄存器我们手写的内联汇编代码中也使用了一些寄存器。
为了通知编译器让它知道: 在内联汇编代码中有哪些寄存器被我们用户使用了可以在这里列举出来这样的话gcc 就会避免使用这些列举出的寄存器
2. 输出和输入操作数列表的格式
在系统中存储变量的地方就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 通过寄存器操作局部变量
#include stdio.h
int main()
{ int data1 1; int data2 2; int data3; asm(movl %%ebx, %%eax\n\t 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), %eaxmovl-16(%ebp), %edxmovl%eax, %ebxmovl%edx, %ecx#APP# 10 test4.c 1movl %ebx, %eaxaddl %ecx, %eax# 0 2#NO_APP movl%eax, -12(%ebp)img
可以看到在进入手写的内联汇编代码之前 把数字 1 通过栈空间(-20(%ebp))复制到寄存器 %eax再复制到寄存器 %ebx; 把数字 2 通过栈空间(-16(%ebp))复制到寄存器 %edx再复制到寄存器 %ecx; 这 2 个操作正是对应了内联汇编代码中的“输入操作数列表”部分b(data1),c(data2)。
在内联汇编代码之后(#NO_APP 之后)把 %eax 寄存器中的值复制到栈中的 -12(%ebp) 位置这个位置正是局部变量 data3 所在的位置这样就完成了输出操作。
4. test5.c 声明改动的寄存器
在 test4.c 中我们没有声明改动的寄存器所以编译器可以任意选择使用哪些寄存器。从生成的汇编代码 test4.s 中可以看到gcc 使用了 %edx 寄存器。
那么我们来测试一下告诉 gcc 不要使用 %edx 寄存器。
#include stdio.h
int main()
{ int data1 1; int data2 2; int data3; asm(movl %%ebx, %%eax\n\t 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), %eaxmovl-16(%ebp), %ecxmovl%eax, %ebx#APP# 10 test5.c 1movl %ebx, %eaxaddl %ecx, %eax# 0 2#NO_APPmovl%eax, -12(%ebp)img
可以看到在内联汇编代码之前gcc 没有选择使用寄存器 %edx。
三、使用占位符来代替寄存器名称
在上面的示例中只使用了 2 个寄存器来操作 2 个局部变量如果操作数有很多那么在内联汇编代码中去写每个寄存器的名称就显得很不方便。
因此扩展 asm 格式为我们提供了另一种偷懒的方法来使用输出和输入操作数列表中的寄存器占位符
占位符有点类似于批处理脚本中利用 2...来引用输入参数一样内联汇编代码中的占位符从输出操作数列表中的寄存器开始从 0 编号一直编号到输入操作数列表中的所有寄存器。
还是看例子比较直接
1. test6.c 使用占位符代替寄存器
#include stdio.h
int main()
{ int data1 1; int data2 2; int data3; asm(addl %1, %2\n\t 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), %eaxmovl-16(%ebp), %edx#APP# 10 test6.c 1addl %eax, %edxmovl %edx, %eax# 0 2#NO_APPmovl%eax, -12(%ebp)img
可以看到gcc 编译器选择了 %eax 来存储局部变量 data1%edx 来存储局部变量 data2 然后操作结果也存储在 %eax 寄存器中。
是不是感觉这样操作就方便多了不用我们来指定使用哪些寄存器直接交给编译器来选择。
在内联汇编代码中使用 %0、%1 、%2 这样的占位符来使用寄存器。
别急如果您觉得使用编号还是麻烦容易出错还有另一个更方便的操作扩展 asm 格式还允许给这些占位符重命名也就是给每一个寄存器起一个别名然后在内联汇编代码中使用别名来操作寄存器。
还是看代码
2. test7.c 给寄存器起别名
#include stdio.h
int main()
{int data1 1;int data2 2;int data3;asm(addl %[v1], %[v2]\n\tmovl %[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。
1. test8.c 使用内存地址来操作数据
#include stdio.h
int main()
{int data1 1;int data2 2;int data3;asm(movl %1, %%eax\n\taddl %2, %%eax\n\tmovl %%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), %eaximg
可以看到在进入内联汇编代码之前把 data1 和 data2 的值放在了栈中然后直接把栈中的数据与寄存器 %eax 进行操作最后再把操作结果(%eax)复制到栈中 data3 的位置(-16(%ebp))。
五、总结
通过以上 8 个示例我们把内联汇编代码中的关键语法规则进行了讲解有了这个基础就可以在内联汇编代码中编写更加复杂的指令了。
希望以上内容对您能有所帮助谢谢
文章中的 8 个示例代码可以在 CPP 开发者 公众号后台回复关键字【 内联汇编示范代码 】即可收到下载地址。