GCC-内联汇编

本文介绍了GCC内联汇编的基本概念,包括内联函数的工作原理、GCC汇编器语法、基本内联汇编指令、扩展汇编的使用(包括操作数、clobberList和volatile关键字),以及如何处理寄存器和约束。

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

概述

内联代表了什么?
我们可以指示编译器将函数代码的代码插入它的调用者里实际被调用的位置,这样的函数叫内联函数,听起来就像宏一样。
内联函数的好处是什么?
可以减少函数调用的开销!!如果任何实际参数值是常量,它们的已知值可以被简化在编译时期,所以不是所有的内联函数的代码需要被包含,对代码量大小的影响是很难预测的,依赖于具体的案例。
现在我们可以猜一猜什么是内联汇编,仅仅只是汇编routines写成内联函数,它们方便,快速,更加有效在系统编程中,我们主要目标是学习基本的格式和GCC内联汇编函数的使用,声明一个内联汇编函数,我们使用关键字asm或者__asm__;
内联汇编之所以重要,主要是因为它能够对C变量进行操作并使其输出可见。由于这种能力,“asm”作为汇编指令和包含它的“C”程序之间的接口。

1 GCC汇编器语法

GCC使用AT&T/UNIX 汇编语法,这里我们使用AT&T,下图比较了它们的不同。

在这里插入图片描述

2 Basic inline

基础的内联汇编是非常简单直接的,它的基本形式就是
asm("assembly code");
Example

asm("movl %ecx %eax")//将寄存器ecx里面的内容移动到eax

如果我们有多条指令,我们每行写一条指令,并且在指令后面加上\n\t,这是因为gcc将每条指令作为字符串发送给as(GAS),并且通过使用换行符/制表符,我们将格式正确的行发送给汇编器。

__asm__("movl %eax,%ebx\n\t"
		"movl $56,%esi\n\t"
		"movl %ecx,$label(%edx,%ebx,$4)\n\t"
		"movb %ah,(%ebx");
		

如果我们的代码touch(比如改变内容)一些寄存器,并且从汇编返回而没有修复这些更改,那么会发生一些不好的事情,这是因为GCC不知道寄存器内容会更改,这会导致麻烦,特别是当编译器进行一些优化时。它将假设某个寄存器包含我们可能已经更改但未通知GCC的某个变量的值,并且它会像什么也没有发生一样继续运行。我们可以使用那些没有副作用的指令,或者在退出时修复问题或等待某些东西崩溃。这就是我们想要一些扩展功能的地方。扩展汇编为我们提供了这种功能。
编译器在进行代码优化时,会对内联汇编语言中的指令进行分析和重排列,以提高程序的执行效率。但是,编译器并不会对内联汇编语言中的寄存器内容进行分析和跟踪,因为它假设内联汇编语言中的指令不会对寄存器内容产生影响。因此,如果在内联汇编语言中更改了某些寄存器的内容,而这些更改没有被告知编译器,那么编译器就不知道这些寄存器的内容已经被更改了,就可能会假设这些寄存器的内容是原来的值,从而导致程序出现错误。因此,在使用内联汇编语言时,需要特别注意寄存器的使用和更改,并使用扩展汇编来告知编译器哪些寄存器的内容已经被更改了。
没怎么理解。。。,大概记住我们改变了寄存器的值需要告诉编译器才正确!!!

3 Extended Asm

在基本的内联汇编中,我们只有指令,在extended assembly,我们可以指定operands;
允许我们指定输入寄存器,输出寄存器和 a list of clobbered 寄存器,指定的 寄存器不是强制要被使用的,we can leave that head ache to GCC and that probably fit into GCC’s optimization scheme better.不管怎样,基本的模板是这样的,

asm(assembler template
	:output operands /*可选的*/
	:input operands  //可选的
	:list of clobber registers //可选的

assember template:汇编器模板是由汇编指令组成
每个operand 是由一个operand-constraint string跟着一个在括号里面的c表达式,一个冒号将汇编器模板与第一个输出操作数分隔开,另一个冒号将最后一个输出操作数与第一个输入(如果有的话)分隔开。每组中的操作数之间用逗号分隔。操作数的总数被限制为10个机器描述中任何指令模式中的最大操作数数,以较大者为准。
Example:

asm("cld\n\t"
	"rep\n\t"
	"stosl"
	:/*no output registers*/
	:"c"(count),"a"(fill_value),"D"(dest)
	:"%eax","%edi"
	);

这段代码做了什么呢?上面的内联填充fill_value count times到寄存器edi指向的位置,它还告诉GCC寄存器eax和edi不在有效,在多看几个例子

int a=10,b;
asm("movl %1,%%eax;\   
	 movl %%eax,%0;"
	 :"=r"(b)    //output
	 :"r"(a)     //input
	 :"%eax"    //clobbered register
	 );

使用\将两行代码连接起来,
我们使用汇编指令使b的值等于a的值,一些有趣的点:
1,”b"是输出操作数,由%0表示,a是输入操作数,由%1表示。
2,"r"是一个在操作数上的约束,我们会在后面进一步讨论约束的细节,目前,“r"告诉了GCC使用任意的寄存器存储操作数,输出操作数约束应该有一个约束修饰符”=”,这个修饰符表示这是一个输出操作数,只能写。
3,在寄存器名前有两个%的前缀。这有助于GCC区分操作数和寄存器。操作数有一个%作为前缀。
4,第三个冒号后面的寄存器%eax告诉GCC %eax的值要在"asm"内部修改,所以GCC不会使用这个寄存器来存储任何其他值。

3.1 汇编器模板

汇编器模板包含了插入c程序的汇编指令集合,格式是这样的:要么每条指令都用双引号括起来,要么整个指令group在双引号内部,每条指令也应该以分隔符结束。有效的分隔符是换行符(\n)和分号(😉。’ \n '后面可以跟一个制表符(\t),C表达式对应的操作数用%0,%1…等。

3.2 操作数

c表达式充当”asm"内部汇编指令的操作数,每个操作数前面都有一个用双引号括起来的操作数约束。
“约束”(c表达式)是一个通用的形式,对于输出操作数,还会有一个额外的修饰符,约束主要用于决定操作数的寻址模式,也可以用来指明被使用的寄存器,使用超过一个一个操作数,用逗号分隔开。
在汇编模板中,每个操作数都由一个数字引用,编号规则是这样:
如果输入,输出操作数总共是n个操作数,第一个output 操作数被编号为0,按顺序递增,最后一个输入操作数的编号为n-1。最多上面有说过了。

输出操作数必须要是左值,输入操作数没有这样的限制,它们可能是表达式, The extended asm feature is most often used for machine instructions the compiler itself does not know as existing 😉. If the output expression cannot be directly addressed (for example, it is a bit-field), our constraint must allow a register. In that case, GCC will use the register as the output of the asm, and then store that register contents into the output(不是太明白)。
根据上面说的,普通的输入操作数必须是write-only,GCC假设在操作数中的值在指令之前是dead and 不需要generated,extended asm 也支持input-output or read-write 操作数。
现在集中看一些例子,我们把一个数乘以5,为此,我们使用lea指令,

asm("leal (%1,%1,4),%0"
	:"=r"(five_time_x)
	:"r"(x)
	);

这里我们的输入是在x,我们没有具体说明我们使用的寄存器,GCC将选择一些寄存器用于input,一个寄存器作为output,如果我们希望输入和输出驻留在同一个寄存器中,我们可以指示GCC这样做。这里我们使用了这些类型的读写操作数。通过指定适当的约束,我们在这里做到了

asm("leal (%0,%0,4),%0"
	:"=r"(five_times_x)
	:"0"(x)
	);

现在输入和输出操作数在同一个寄存器中,但是我们不知道在哪个寄存器,如果我们还想指定在哪些寄存器,我们可以这么做,

asm("leal (%%ecx,%%ecx,4),%%ecx"
	:"=c"(x)
	:"c"(x)
	);

上面的三个案例,我们都没有放置任何寄存器在clobber list。为什么呢?在前面两个例子里,GCC决定寄存器,它知道哪些寄存器改变了,在最后一个例子里面,我们没有把ecx 放在clobber list里,GCC知道it goes into x,因此它可以知道ecx的值,it isn’t considered clobbered.

3.3 clobber List

一些指令clobber一些硬件寄存器,我们必须list这些寄存器在clobber-list,比如在asm function 的第三个‘:后面,这是通知GCC,我们自己将使用和修改它们,So GCC不会认为它加载到这些寄存器的值是有效的,我们不应该列出输入和输出寄存器在这个列表中,因为GCC知道”asm"使用它们(因为它们被显式地指定为约束)。如果指令使用别的任何寄存器,隐式的或者显式的(并且这些寄存器既不在input也不再output约束列表里),那么这些寄存器必须在clobber list里具体说明。
如果我们的指令可以改变条件码寄存器,我们必须add “cc"到clobber registers list 里面。
如果我们的指令以不可预知的方式修改内存,add “memory” to the list of clobbered registers;
这将导致GCC不在整个汇编指令的寄存器中保存缓存的内存值。
如果asm的输入或输出中没有列出受影响的内存,我们还必须添加volatile关键字。
我们可以随意的读写这些clobbered registers,考虑在模板中有多个指令的例子,it assumes subroutine_foo accept arguments 在寄存器eax和ecx.

asm("movl %0,%%eax;\
	movl %1,%%eax;\
	call _foo"
	://no outputs
	:"g"(from),"g"(to)
	:"eax","ecx"
	);

3.4 Volatile

如果你熟悉内核源代码或类似的漂亮代码,你一定见过许多函数声明为volatile或__volatile__跟着asm或__asm__ 后面。什么是易失性?
如果我们的汇编语句必须在我们放置的地方执行(比如,一定不能移出循环作为优化),在asm后面()前面加关键字volatile.防止它被移动或者删除或者其他。
asm volatile(… : … : … : …);
如果我们的程序集只是用于进行一些计算,并且没有任何副作用,那么最好不要使用关键字volatile。避免它有助于gcc优化代码并使其更美观

4 进一步关于约束

关于约束,我们前面说的很少。自己看吧,链接挂下面了

5 具体的例子

[1]GCC-Inline-Assembly-HOWTO

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值