0%

GCC内联汇编笔记

什么是内联汇编?

内联函数
我们可以要求编译器将一个函数的代码插入到调用者的代码中函数实际调用的地方

inline(内联)的作用
减少函数调用开销, 同时如果所有实参的值为常量, 它们的已知值可以再编译期允许简化, 因此并非所有的内联函数代码都需要被包含进去, 代码大小的影响是不可预测的, 这取决于特定的情况

内联汇编
写为内联函数的汇编程序
可以操作并且使其输出通过通过C变量显示出来, 因此asm可作为汇编指令和包含它的C程序之间的接口

源操作数和目的操作数顺序

Op-code src dst (Intel语法相反)

寄存器命名

%eax

立即数

$0xFFFF

操作数大小

取决于操作码名字的最后一个字符

AT&T b w l
字节 长型
8 16 32
Intel byte word dword
ptr ptr ptr
AT&T movb foo, %al
Intel mov al, byte ptr foo

存储器操作数

AT&T section:disp(base, index, scale)
Intel section:[base + index*scale + disp]

注意: 当一个常量用于disp或scale, 不能添加$前缀

base: 基址寄存器 ebx
index: 索引(寄存器)
scale: 比例(数值)
disp: 位移(数值)

基本内联

asm(“汇编代码”); 或者 asm(“汇编代码”);
采用后者的原因: asm和标识符冲突时, 或是为了与ANSI C兼容

指令多于一条

  • 每个一行
  • 用双引号圈起
  • 每条指令添加后缀\n\t
    • gcc将每一条当做字符串发送给as(GAS, GNU汇编器), 并且通过使用\n\t发送正确格式化后的行给汇编器

e.g.

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

扩展汇编

为什么需要扩展汇编?

如果在代码中, 我们涉及到一些寄存器(即改变其内容), 但在没有恢复这些变化的情况下从汇编中返回, 将会导致一些意想不到的事情.
这是因为GCC并不知道寄存器内容的变化, 这会导致问题, 特别是当编译器做了某些优化.
在没有告知GCC的情况下, 他将会假设一些寄存器存储了一些值, 而我们可能已经改变却没有告知GCC, 它会像什么事都没发生一样(GCC不会假设寄存器装入的值是有效的, 当退出改变了寄存器值的内联汇编后, 寄存器的值不会保存到相应的变量或内存空间)继续运行.
我们可以做的是, 使用那些没有副作用的指令, 或者当我们退出时恢复这些寄存器, 要不就等着程序崩溃.
这就是为什么我们需要一些扩展功能, 扩展汇编给我们提供了那些功能.

扩展汇编的使用

扩展汇编中, 我们可以同时指定操作数.
它允许我们指定输入寄存器, 输出寄存器以及修饰寄存器列表.
GCC不强制用户必须指定使用的寄存器.
我们可以把头疼的事留给GCC, 这可能可以更好地适应GCC的优化.

结构:

1
2
3
4
asm(汇编语句
:输出操作数/*可选的*/
:输入操作数/*可选的*/
:修饰寄存器列表/*可选的*/);

每一个操作数由一个操作数字符串所描述, 其后紧接一个括弧括起的C表达式.
逗号用于分离每一个组内的操作数.
总操作数的数目限制在10个, 或者机器描述中的任何指令格式中的最大操作数数目, 以较大者为准.
如果没有输出操作数但存在输入操作数, 你必须将两个连续的冒号放置于输出操作数原本会放置的地方周围.

e.g.

1
2
3
4
5
6
7
//使用汇编指令使b变量的值等于a变量的值
int a=10, b;
asm("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /*输出*/
:"r"(a) /*输入*/
:"%eax"); /*修饰寄存器*/

注意:

  1. b为输出操作数, 用%0引用; a为输入操作数, 用%1引用
  2. r为操作数约束, r告诉GCC可以使用任意寄存器存储操作数; 输出操作数应该有一个约束修饰符=, 表明它是一个只写的输出操作数.
  3. 寄存器名字以%%为前缀, 这有利于GCC区分操作数和寄存器, 操作数以%为前缀; 第三个冒号之后的修饰寄存器%eax用于告诉GCC%eax的值将会在asm内部被修改, 所以GCC将不会使用此寄存器存储任何其他值.
  4. 当asm执行完毕, b变量会映射到更新的值, 因为它被指定为输出操作数; 换句话说asm内b变量的修改应该会被映射到asm外部.

注意事项

约束字符串主要用于决定操作数的寻址方式, 同时也用于指定使用的寄存器.

输出操作数表达式必须为左值.
输入操作数的要求不像这样严格.
他们可以为表达式.
扩展汇编特性常常用于编译器所不知道的机器指令.
如果输出表达式无法直接寻址(即, 它是一个位域), 我们的约束字符串必须给定一个寄存器.
在这种情况下, GCC将会使用该寄存器作为汇编的输出, 然后存储该寄存器的内容到输出.

普通的输出操作数必须为只写的.
GCC将会假设指令前的操作数值是死的, 并且不需要被(提前)生成.
扩展汇编也支持输入-输出或者读-写操作数.

如果我们的指令可以修改条件码寄存器(cc), 我们必须将cc添加进修饰寄存器列表.
如果我们的指令, 以不可预测的方式修改了内存, 那么需要将memory添加进修饰寄存器列表.
这可以使GCC不会在汇编指令间保持缓存于寄存器的内存值.
如果被影响的内存不在汇编的输入或输出列表中, 我们也必须添加volatile关键字.

volatile

如果我们的汇编语句必须在我们放置它的地方执行(如不能为了优化而被移出循环语句), 将关键词volatile放置在asm后面, ()前面.
以防止它被移动, 删除或者其他操作, 我们将其声明为asm volatile(... :... :... :...);
如果担心发生冲突, 请使用__volatile__.

如果我们的汇编只是用于一些计算并且没有任何副作用, 不使用volatile关键词会更好.
不使用volatile可以帮助gcc优化代码并使代码更漂亮.

常用约束

寄存器操作数约束

GPR(General Purpose Register, 通用寄存器)

要指定寄存器, 你必须使用特定寄存器约束直接地指定寄存器的名字, 它们为:

r Register(s)
a %eax, %ax, %al
b %ebx, %bx, %bl
c %ecx, %cx, %cl
d %edx, %dx, %dl
S %esi, %si
D %edi, %di

内存操作数约束

当操作数位于内存时, 任何对它们的操作将直接发生在内存位置, 这与寄存器约束相反, 后者首先将值存储在要修改的寄存器中, 然后将它写回内存位置.
但寄存器约束通常用于一个指令必须使用它们或者它们可以大大提高处理速度的地方.
当需要在asm内更新一个C变量, 而又不是使用寄存器去保存它的值, 使用内存最为有效.

e.g.

1
2
//IDTR寄存器的值存储于内存位置loc处
asm("sidt %0\n"::"m"(loc));

注意: 每条指令应以分界符结尾, 有效的分界符有换行符\n和分号;, \n可以紧随一个制表符\t.

匹配(数字)约束

在某些情况下, 一个变量可能既充当输入操作数, 也充当输出操作数.
可以通过使用匹配约束在asm中指定这种情况.

e.g.

1
asm("incl %0" :"=a"(var) :"0"(var));

说明:
寄存器%eax既用作输入变量, 也用作输出变量.
var输入被读进%eax, 并且等递增后更新的%eax再次被存储进var.
这里的0用于指定与第0个输出变量相同的约束.
它指定var输出示例应只被存储在%eax中.
该约束可用于:

  • 在输入从变量读取或变量修改后且修改被写回同一变量的情况
  • 在不需要将输入操作数实例和输出操作数实例分开的情况

使用匹配约束最重要的意义在于它们可以有效地使用可用寄存器.

其他一些约束

约束 说明
m 允许一个内存操作数, 可以使用机器普遍支持的任一种地址
o 允许一个内存操作数, 但只有当地址是可偏移的; 即, 该地址加上一个小的偏移量可以得到一个有效地址
V 一个不允许偏移的内存操作数; 换言之, 任何适合m约束而不适合o约束的操作数
i 允许一个(带有常量)的立即整型操作数; 这包括其值仅在汇编时期知道的符号常量
n 允许一个带有已知数字的立即整型操作数; 许多系统不支持汇编时期的常量, 因为操作数少于一个字宽; 对于此种操作数, 约束应该使用n而不是i
g 允许任一寄存器, 内存或者立即整型操作数, 不包括通用寄存器之外的寄存器

x86特有约束

约束 说明
r 寄存器操作数约束, 查看上面的给定的表格
q 寄存器a, b, c, 或者d
l 范围从0到31的常量(对于32位移位)
J 范围从0到63的常量(对于64位移位)
K 0xff
L 0xffff
M 0, 1, 2 或 3(lea指令的移位)
N 范围从0到255的常量(对于out指令)
f 浮点寄存器
t 第一个(栈顶)浮点寄存器
u 第二个浮点寄存器
A 指定ad寄存器; 这主要用于想要返回64位整型数, 使用d寄存器保存最高有效位和a寄存器保存最低有效位

约束修饰符

当使用约束时, 对于更精确的控制超过了对约束作用的需求, GCC给我们提供了约束修饰符.

约束修饰符 I/O 说明
= O 对于这条指令, 操作数为只写的; 旧值会被忽略并被输出数据所替换; 只能写在第一个字符的位置
& O 用符号&进行修饰时,等于向GCC声明, “GCC不得为任何Input操作表达式分配与此Output操作表达式相同的寄存器”; 其原因是修饰符&意味着被其修饰的Output操作表达式要在所有的Input操作表达式被输入之前输出; 即, GCC会先使用输出值对被修饰符&修饰的Output操作表达式进行填充,然后,才对Input操作表达式进行输入; 这样的话,如果不使用修饰符&对Output操作表达式进行修饰,一旦后面的Input操作表达式使用了与Output操作表达式相同的寄存器,就会产生输入输出数据混乱的情况; 相反,如果用修饰符&修饰输出操作表达式, 那么, 就意味着GCC会先把Input操作表达式的值输入到选定的寄存器中,然后经过处理,最后才用输出值填充对应的Output操作表达式; 只能写在第二个字符的位置; 参考自https://blog.csdn.net/koozxcv/article/details/49612791
+ O 对于这条指令, 操作数为可读可写的; 只能写在第一个字符的位置
% I 表示此Input操作表达式中的C/C++表达式可以与下一个Input操作表达式中的C/C++表达式互换
Thank you for your reward !