内联汇编

volatile 或 __volatile__ 是可选的,可以将它们添加到 asm 后面,禁止某些编译器的优化

asm 和 __asm__ 几乎是相同的,惟一的区别是,当预处理程序宏中使用内联汇编时,asm 在编译过程中可能会引发警告。volatile 和 __volatile__ 也是如此


基本内联汇编

基本内联汇编格式比较直观,可以直接这样写:

__asm__("assembly code");

如果在内联代码中操作了一些寄存器,比如你修改了寄存器内容(而之后也没有进行还原操作),程序很可能会产生一些难以预料的情况。因为此时GCC并不知道你已经将寄存器内容修改了。这点尤其是在编译器对代码进行了一些优化的情况下而导致问题。因为编译器注意不到寄存器内容已经被改掉,程序将当作它没有被修改过而继续执行。所以此时我们尽量不要使用这些会产生附加影响的操作,或者当我们退出的时候还原这些操作。否则很可能会造成程序崩溃。可是如果我们必须要这样操作该怎么办呢?我们可以通过下面的讨论的扩展内联汇编进行


扩展内联汇编

asm ( assembler template
        : output operands                /* optional */
        : input operands                   /* optional */
        : list of clobbered registers   /* optional */
);

其中assembler template为汇编指令部分。括号内的操作数都是C语言表达式中常量字符串。不同部分之间使用冒号分隔。相同部分语句中的每个小部分用逗号分隔。最多可以指定10个操作数,不过可能有的计算机平台有额外的文档说明可以使用超过10个操作数

此外,如果没有输出部分但是有输入部分,我们还得保留输出部分前面的冒号。就像下面这样:

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

再一个例子

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

上面代码实现的功能就是用汇编代码把a的值赋给b。值得注意的几点有:

"b" 是输出操作数,用%0来访问,”a”是输入操作数,用%1来访问。 

"r" 是一个constraint, 关于constraint后面有详细的介绍。这里我们只要记住这里”r”的意思就是让GCC自己去选择一个寄存器去存储变量a。输出部分constraint前必须要有个 ”=”修饰,用来说明是一个这是一个输出操作数,并且是只写(write only)的。 

你可能已经注意到,有的寄存器名字前面用了”%%”,这是用来让GCC区分操作数和寄存器的:操作数已经用了一个%作为前缀,寄存器只能用“%%”做前缀了。 

第三个冒号后面的clobbered register部分有个%eax,意思是内联汇编代码中会改变寄存器eax的内容,如此一来GCC在调用内联汇编前就不会依赖保存在寄存器eax中的内容了



汇编模板

__asm__ __volatile__ ("lwarx %0, 0, %1 \n\t" : "=&r"(ret) : "r"(p));

汇编指令由操作码 (lwarx) 和操作数 (%0, 0, %1) 组成。

如果一个指令的操作数是寄存器/立即类型的操作数,那么可以引用它作为一个带有百分比前缀编号的寄存器。 (%0, %1,...) 

寄存器的编号引用了一个变量,按它在输入/输出列表中所代表的顺序排列。在代号 D 的示例中,ret 是输入/输出列表中第一个被引用的变量。因此,%0 是寄存器引用。同样地,寄存器 %1 引用变量 p。

输入输出操作数列表

输入/输出列表以冒号 (:) 开始。它们的条目用逗号 (,) 隔开。该列表在汇编模板中指定变量及其约束。以代码 D 为例,lwarx 设置有效地址,即寄存器值 %1 加上一个立即值 0。它从有效地址读取一个单词并存储到寄存器 %0。在这里,%0 是一个输出操作数,它存储结果并被写入列表。而 %1 是一个输入。这样,%0 所引用的 ret 就会放入输出列表,而 %1 所引用的 p 则会放入输入列表。 输入/输出操作数列表中列出的每个变量


constraint约束修饰符

r    通用寄存器

=   指明这个操作数是只写的;之前保存在其中的值将被废弃而被输出值所代替

&   指明这个操作事数是一个会在使用之前被修改的操作数,这个操作数将在输入指令用过输入操作数之前被修改。因此,该操作数不能被放在一个被用作输入操作数的寄存器或者内存处。只有在该操作数被写入之前完成输入指令的情况下,可以被绑定在该操作数上 

m   使用一个内存操作数,内存地址可以是机器支持的范围内

o    使用一个内存操作数,但是要求内存地址范围在在同一段内。例如,加上一个小的偏移量来形成一个可用的地址

V    内存操作数,但是不在同一个段内。换句话说,就是使用除了”o” 以外的”m”的所有的情况

i     使用一个立即整数操作数(值固定);也包含仅在编译时才能确定其值的符号常量

n    一个确定值的立即数。很多系统不支持汇编常数操作数小于一个字(word)的长度的情况。这时候使用n就比使用i好

g    除了通用寄存器以外的任何寄存器,内存和立即整数

详细约束 gcc constrains


破坏列表(Clobber list)

乱码列表通知编译器,有些寄存器或内存已因内联汇编块造成乱码。乱码列表看起来类似于输入/输出列表(用冒号开始,并以逗号分隔)。但只用寄存器名称(如 r1、f15)或 内存 充当其条目。

在代码 C 的示例中,内联汇编代码隐式地破坏了条件寄存器字段。因此,cr0 寄存器字段被放入破坏列表。如果用户认为代码更换到了一个不确定的内存空间,那么内存也会出现在列表中。我们在后面的章节将再次讨论破坏列表。 

事实上,并不是所有在清单 C 中显示的组件都是必需的。一个关键字和一个汇编模板就足以构成一个基本的内联汇编。其他所有部分都是可选的。


疑难解释

//栈顶数据存入 rcx 寄存器,bootstrap.argc bootstrap.argv 赋值由 rcx 寄存器作为中介
__asm__ ("mov 0x08(%%rbp), %%rcx " : "=c" (bootstrap.argc));
__asm__ ("lea 0x10(%%rbp), %%rcx " : "=c" (bootstrap.argv));

对应的汇编

  4008e0: 48 8b 4d 08           mov    0x8(%rbp),%rcx
  4008e4: 89 c8                 mov    %ecx,%eax
  4008e6: 89 45 f0              mov    %eax,-0x10(%rbp)
  4008e9: 48 8d 4d 10           lea    0x10(%rbp),%rcx
  4008ed: 48 89 c8              mov    %rcx,%rax
  4008f0: 48 89 45 f8           mov    %rax,-0x8(%rbp)
  4008f4: 48 8d 7d f0           lea    -0x10(%rbp),%rdi
  4008f8: e8 87 ff ff ff        callq  400884 <do_main>



上一篇: x86指令参考网站
下一篇: CPU寄存器
作者邮箱: 203328517@qq.com