0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

内联汇编很可难吗 看完这篇文章就能搞定!

Q4MP_gh_c472c21 来源:IOT物联网小镇 作者:道哥 2021-04-29 13:57 次阅读

一、基本 asm 格式

1. 语法规则

2.test1.c 插入空指令

3. test2.c操作全局变量

4. test3.c 尝试操作局部变量

二、扩展 asm 格式

1. 指令格式

2.输出和输入操作数列表

3.test4.c通过寄存器操作局部变量

4. test5.c声明使用的寄存器

三、使用占位符来代替寄存器名称

1. test6.c使用占位符代替寄存器名

2. test7.c 给寄存器起别名

四、使用内存地址

1.test8.c 使用内存地址来操作数据

五、总结

Linux 代码中,经常可以看到在 C 代码中,嵌入部分汇编代码,这些代码要么是与硬件体系相关的,要么是对性能有关键影响的。

在很久以前,我特别惧怕内嵌汇编代码,直到后来把汇编部分的短板补上之后,才彻底终结这种心理。

也许你在工作中,几乎不会涉及到内嵌汇编代码的工作,但是一旦进入到系统的底层,或者需要对时间关键场景进行优化,这个时候你的知识储备就发挥重要作用了!

这篇文章,我们就来详细聊一聊在 C 语言中,如何通过asm 关键字来嵌入汇编语言代码,文中的 8 个示例代码从简单到复杂,逐步深入地介绍内联汇编的关键语法规则。

希望这篇文章能够成为你进阶高手路上的垫脚石!

PS:

示例代码中使用的是 Linux 系统中 AT&T 汇编语法;

文章中的 8 个示例代码,可以在公众号后台回复【426】,即可收到下载地址;

一、基本 asm 格式

gcc 编译器支持2 种形式的内联 asm 代码:

基本 asm 格式:不支持操作数;

扩展 asm 格式:支持操作数;

1. 语法规则

asm [volatile] ("汇编指令")

所有指令,必须用双引号包裹起来;

超过一条指令,必须用 分隔符进行分割,为了排版,一般会加上 ;

多条汇编指令,可以写在一行,也可以写在多行;

关键字 asm 可以使用asm来替换;

volatile 是可选的,编译器有可能对汇编代码进行优化,使用 volatile 关键字之后,告诉编译器不要优化手写的内联汇编代码。

2. test1.c 插入空指令

#include 
int main()
{
    asm ("nop");
    printf("hello
");
    asm ("nop
	nop
	"
 "nop");
    return 0;
}

注意:C语言中会自动把两个连续的字符串字面量拼接成一个,所以"nop nop " "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 编译器生成的汇编代码中包含了这两部分代码。

这 2 部分嵌入的汇编代码都是空指令 nop,没有什么意义。

3. test2.c 操作全局变量

在 C 代码中嵌入汇编指令,目的是用来计算,或者执行一定的功能,下面我们就来看一下,如何在内联汇编指令中,操作全局变量。

#include 

int a = 1;
int b = 2;
int c;

int main()
{
    asm volatile ("movl a, %eax
	"
        "addl b, %eax
	"
        "movl %eax, c");
    printf("c = %d 
", 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 中;

3f087b10-a86f-11eb-9728-12bb97331649.png

生成汇编代码指令:

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"
.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 
int main()
{
    int a = 1;
    int b = 2;
    int c;

    asm("movl a, %eax
	"
        "addl b, %eax
	"
        "movl %eax, c");
    printf("c = %d 
", 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)

40e39028-a86f-11eb-9728-12bb97331649.png

我们来尝试编译成可执行程序:

$ gcc -m32 -o test3 test3.c
/tmp/ccuY0TOB.o: In function `main':
test3.c undefined reference to `a'
test3.c undefined reference to `b'
test3.c undefined reference to `c'
collect2: 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 

int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("movl %%ebx, %%eax
	"
        "addl %%ecx, %%eax"
        : "=a"(data3)
        : "b"(data1),"c"(data2));

    printf("data3 = %d 
", 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)

411b1994-a86f-11eb-9728-12bb97331649.png

可以看到,在进入手写的内联汇编代码之前:

把数字 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 
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("movl %%ebx, %%eax
	"
        "addl %%ecx, %%eax"
        : "=a"(data3)
        : "b"(data1),"c"(data2)
        : "%edx");

    printf("data3 = %d 
", 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)

415f4d94-a86f-11eb-9728-12bb97331649.png

可以看到,在内联汇编代码之前,gcc没有选择使用寄存器%edx。

三、使用占位符来代替寄存器名称

在上面的示例中,只使用了2 个寄存器来操作 2 个局部变量,如果操作数有很多,那么在内联汇编代码中去写每个寄存器的名称,就显得很不方便。

因此,扩展 asm 格式为我们提供了另一种偷懒的方法,来使用输出和输入操作数列表中的寄存器:占位符!

占位符有点类似于批处理脚本中,利用2...来引用输入参数一样,内联汇编代码中的占位符,从输出操作数列表中的寄存器开始从0编号,一直编号到输入操作数列表中的所有寄存器。

还是看例子比较直接!

1. test6.c 使用占位符代替寄存器

#include 
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("addl %1, %2
	"
        "movl %2, %0"
        : "=r"(data3)
        : "r"(data1),"r"(data2));

    printf("data3 = %d 
", 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)

422d3ac4-a86f-11eb-9728-12bb97331649.png

可以看到,gcc 编译器选择了%eax来存储局部变量 data1,%edx来存储局部变量 data2,然后操作结果也存储在%eax 寄存器中。

是不是感觉这样操作就方便多了?不用我们来指定使用哪些寄存器,直接交给编译器来选择。

在内联汇编代码中,使用%0、%1 、%2这样的占位符来使用寄存器。

别急,如果您觉得使用编号还是麻烦,容易出错,还有另一个更方便的操作:扩展 asm 格式还允许给这些占位符重命名,也就是给每一个寄存器起一个别名,然后在内联汇编代码中使用别名来操作寄存器。

还是看代码!

2. test7.c 给寄存器起别名

#include 
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("addl %[v1], %[v2]
	"
        "movl %[v2], %[v3]"
        : [v3]"=r"(data3)
        : [v1]"r"(data1),[v2]"r"(data2));

    printf("data3 = %d 
", 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 
int main()
{
    int data1 = 1;
    int data2 = 2;
    int data3;

    asm("movl %1, %%eax
	"
        "addl %2, %%eax
	"
        "movl %%eax, %0"
        : "=m"(data3)
        : "m"(data1),"m"(data2));

    printf("data3 = %d 
", 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

425d8d64-a86f-11eb-9728-12bb97331649.png

可以看到:在进入内联汇编代码之前,把data1 和 data2的值放在了栈中,然后直接把栈中的数据与寄存器%eax进行操作,最后再把操作结果(%eax),复制到栈中data3 的位置(-16(%ebp))。

五、总结

通过以上 8 个示例,我们把内联汇编代码中的关键语法规则进行了讲解,有了这个基础,就可以在内联汇编代码中编写更加复杂的指令了。

希望以上内容对您能有所帮助!谢谢!

编辑:jq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    30

    文章

    5020

    浏览量

    117630
  • ASM
    ASM
    +关注

    关注

    1

    文章

    65

    浏览量

    18889
  • 代码
    +关注

    关注

    30

    文章

    4553

    浏览量

    66665
  • 编译器
    +关注

    关注

    1

    文章

    1570

    浏览量

    48604

原文标题:内联汇编很可怕吗?看完这篇文章,终结它!

文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    qemu入门不难

    大佬闪,QEMU在win11下面好不好装,入门不难。
    发表于 04-14 06:58

    KUKA C4机器人:为宏启用或阻止内联窗体USER

    对于不同的宏类型,可以配置内联表单USER是否可用。默认情况下,USER在以下宏中可用
    的头像 发表于 12-18 11:38 519次阅读
    KUKA C4机器人:为宏启用或阻止<b class='flag-5'>内联</b>窗体USER

    学会这4招,轻松搞定开关电源EMI

    学会这4招,轻松搞定开关电源EMI
    的头像 发表于 12-05 16:57 277次阅读
    学会这4招,轻松<b class='flag-5'>搞定</b>开关电源EMI

    一周搞定系列之模电数电

    电子发烧友网站提供《一周搞定系列之模电数电.ppt》资料免费下载
    发表于 11-17 10:01 3次下载
    一周<b class='flag-5'>搞定</b>系列之模电数电

    宏的缺陷与内联函数的引入

    。 所以为了解决这种不利于调试的问题,就有了内联函数。 那么什么是内联函数呢? 我们以inline修饰的函数叫做内联函数,编译阶段,C编译器会在调用函数的地方直接把函数展开,没有压栈开销,内联
    的头像 发表于 11-01 17:57 226次阅读

    多轴伺服,一芯搞定

    多轴伺服,一芯搞定
    的头像 发表于 10-19 17:54 324次阅读
    多轴伺服,一芯<b class='flag-5'>搞定</b>

    ARM开发人员套件1.2版汇编指南

    ARM开发者套件(ADS)拥有: ·一个独立的汇编程序ARMASM·一个内置在C和C++编译器中的优化内联汇编程序。 这些汇编器作为输入的语言基本上是相同的。 但是,在
    发表于 09-01 10:41

    171从ADS迁移到RVDS 3.0的应用笔记

    并不是一个要求。 某些ARM工具特定功能已更改或已过时,需要更改这些功能。 内联汇编程序(__ASM)C编译器有一个内置的内联汇编程序。 然而,ARM体系结构6和更高版本的指令集不会保
    发表于 08-30 08:17

    关于NucleiStudio IDE编译汇编文件的问题

    关于NucleiStudio的使用,我想问两个问题,一个是NucleiStudio IDE编译汇编文件的问题,另一个是关于NucleiStudio IDE编译RISCV向量扩展指令的问题,我在百度
    发表于 08-12 08:05

    RealView开发工具包汇编程序指南

    RealView编译工具(RVCT)具有: ·一个独立的汇编程序,ARMASM·优化C和C++编译器中内置的内联和嵌入式汇编程序。 这些汇编器作为输入的语言基本上是相同的。 但是,在
    发表于 08-12 07:30

    NucleiStudio是否支持修改编译器使得支持自定义指令格式从而能嵌入对应的内联汇编

    NucleiStudio 是否支持修改编译器使得支持自定义指令格式,从而能嵌入对应的内联汇编
    发表于 08-11 11:36

    为什么要反汇编?反汇编文件的生成和解读

    汇编顾名思义就是汇编的逆过程,将二进制文件反汇编汇编代码。arm-linux-objdump是交叉编译工具链里的一个工具,专门用来反汇编
    发表于 08-02 10:25 2849次阅读
    为什么要反<b class='flag-5'>汇编</b>?反<b class='flag-5'>汇编</b>文件的生成和解读

    ESP8266 HTML工作正常,不能内联CSS是为什么?

    内联 CSS 的示例: “This is a red heading with a black background\" 我无法让 C++ IDE 像这样解释该行是。有解决办法吗?
    发表于 06-12 08:07

    为何Linus一个人就能写出这么强的系统

    毕竟一个操作系统的产生、发展和繁荣涉及到的问题还挺多的,比如环境、比如开源、比如生态,甚至还包括一些历史进程和天时地利的影响,所以肯定不是说全靠某一个人信手拈来就能搞定的。
    的头像 发表于 05-12 10:05 517次阅读
    为何Linus一个人<b class='flag-5'>就能</b>写出这么强的系统

    在Rust中使用内联汇编

    与 GCC 内联汇编语法一样,Rust 希望即使需要手写汇编,程序员也能将一部分工作交给编译器来高效完成,这部分工作就是寄存器分配,毕竟只有编译器了解内联
    的头像 发表于 05-04 09:54 560次阅读