跳转至

Inline Assembly

GCC内联汇编

__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
  1. __asm__或asm 用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。
  2. __volatile__或volatile 是可选的。如果用了它,则是向GCC 声明不允许对该内联汇编优化,否则当 使用了优化选项(-O)进行编译时,GCC 将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。
  3. Instruction List 是汇编指令序列。它可以是空的,比如:__asm__ __volatile__(""); 或 __asm__ ("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。
  4. 但并非所有Instruction List 为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory");就非常有意义,它向GCC 声明:“内存作了改动”,GCC 在编译的时候,会将此因素考虑进去。
  5. 当在"Instruction List"中有多条指令的时候,需要用分号(;)或换行符(\n)将它们分开。
  6. 指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符表示的操作数,总被视为long型(4个字节),
    1. 但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节
    2. 对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,"b"代表低字节,"h"代表高字节,例如:%h1
  7. Output/Input
    1. 格式为形如"constraint"(variable)的列表(逗号分隔)。按照出现的顺序分别与指令操作数"%0","%1"对应
    2. 每个输出操作数的限定字符串必须包含"="表示他是一个输出操作数。例子"=r" (value)
  8. Clobber/Modify(由逗号格开的字符串组成)
    1. 在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作表达式使用"r"约束,让GCC为你选择一个寄存器时,GCC知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。
    2. 但是对于"Instruction List"中的临时寄存器,需要在Clobber/Modify域声明这些寄存器或内存,让GCC知道修改了他们
      1. 例子:__asm__ ("mov R0, #0x34" : : : "R0");寄存器R0出现在"Instruction List中",并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定"R0",以让GCC知道这一点。
    3. Clobber/Modify域存在"memory",那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个 时候寄存器中的拷贝已经很可能和内存处的内容不一致了。

输入输出与指令的对应关系

寄存器约束符Operation Constraint

每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint,这里将讨论在80386平台上所可能使用的操作约束。

当前的输入或输出需要借助一个寄存器时,需要为其指定一个寄存器约束,可以直接指定一个寄存器的名字。

常用的寄存器约束的缩写 约束 | 意义| |--------- | ----| r |表示使用一个通用寄存器,由 GCC 在%eax/%ax/%al,%ebx/%bx/%bl,%ecx/%cx/%cl,%edx/%dx/%dl中选取一个GCC认为合适的。 g |表示使用任意一个寄存器,由GCC在所有的可以使用的寄存器中选取一个GCC认为合适的。 q |表示使用一个通用寄存器,和约束r的意义相同。 a |表示使用%eax/%ax/%al b |表示使用%ebx/%bx/%bl c |表示使用%ecx/%cx/%cl d |表示使用%edx/%dx/%dl D |表示使用%edi/%di S |表示使用%esi/%si f |表示使用浮点寄存器 t |表示使用第一个浮点寄存器 u |表示使用第二个浮点寄存器

分类 |限定符 |描述 |--------- | ----|----------------| 通用寄存器 |“a”| 将输入变量放入eax 这里有一个问题:假设eax已经被使用,那怎么办?其实很简单:因为GCC 知道eax 已经被使用,它在这段汇编代码的起始处插入一条语句pushl %eax,将eax 内容保存到堆栈,然 后在这段代码结束处再增加一条语句popl %eax,恢复eax的内容 ||“b” |将输入变量放入ebx ||“c” |将输入变量放入ecx ||“d” |将输入变量放入edx ||“s” |将输入变量放入esi ||“d” |将输入变量放入edi ||“q” |将输入变量放入eax,ebx,ecx,edx中的一个 ||“r” |将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个 ||"A"|把eax和edx合成一个64 位的寄存器(use long longs) 内存| “m” |内存变量 ||“o” |操作数为内存变量,但是其寻址方式是偏移量类型, 也即是基址寻址,或者是基址加变址寻址 ||“V”| 操作数为内存变量,但寻址方式不是偏移量类型 ||" "| 操作数为内存变量,但寻址方式为自动增量 ||“p” |操作数是一个合法的内存地址(指针) 寄存器或内存| “g” |将输入变量放入eax,ebx,ecx,edx中的一个 或者作为内存变量 ||“X” |操作数可以是任何类型 立即数 |“I” |0-31之间的立即数(用于32位移位指令) ||“J” |0-63之间的立即数(用于64位移位指令) ||“N” |0-255之间的立即数(用于out指令) ||“i” |立即数 ||“n” |立即数,有些系统不支持除字以外的立即数, 这些系统应该使用"n"而不是"i" 匹配 |" 0 ",“1” …“9” |, 表示用它限制的操作数与某个指定的操作数匹配,也即该操作数就是指定的那个操作数,例如"0"去描述"%1"操作数,那么"%1"引用的其实就是"%0"操作数,注意作为限定符字母的0-9 与 指令中的"%0"-"%9"的区别,前者描述操作数,后者代表操作数。 ||&; |该输出操作数不能使用过和输入操作数相同的寄存器 |操作数类型 |“=” |操作数在指令中是只写的(输出操作数) ||“+” |操作数在指令中是读写类型的(输入输出操作数) 浮点数| “f” |浮点寄存器 ||“t” |第一个浮点寄存器 ||“u” |第二个浮点寄存器| ||“G” |标准的80387浮点常数 ||% |该操作数可以和下一个操作数交换位置.例如addl的两个操作数可以交换顺序 (当然两个操作数都不能是立即数) ||# |部分注释,从该字符到其后的逗号之间所有字母被忽略 ||* |表示如果选用寄存器,则其后的字母被忽略

内存约束

如果一个Input/Output 操作表达式的C/C++表达式表现为一个内存地址,不想借助于任何寄存器,则可以使用内存约束。比如:

__asm__("lidt%0":"=m"(__idt_addr));
__asm__("lidt%0"::"m"(__idt_addr));

修饰符 |输入/输出 |意义 |---- | --- |--- = | O |表示此Output操作表达式是Write-Only的。 + | O |表示此Output操作表达式是Read-Write的。 & | O |表示此Output操作表达式独占为其指定的寄存器。 % | I |表示此Input 操作表达式中的C/C++表达式可以和下一 个Input操作表达式中的C/C++表达式互换

例子

Static __inline__ void __set_bit(int nr, volatile void * addr)
{
         __asm__(
                         "btsl %1,%0"
                         :"=m" (ADDR)
                         :"Ir" (nr));
}

第一个占位符%0与C 语言变量ADDR对应,第二个占位符%1与C语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价:btsl nr, ADDR

Clobber/Modify域存在"memory"的其他影响

使用"memory"是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。

例如:

int main(int __argc, char* __argv[]) 
{ 
    int* __p = (int*)__argc; 
    (*__p) = 9999; 
    __asm__("":::"memory"); 
    if((*__p) == 9999) 
        return 5; 
    return (*__p); 
}
本例中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相关代码,而不会生成return (*__p)的相关代码。

但你加上了这条内联汇编语句,它除了声明内存变化之外,什么都没有做。

但GCC此时就不能简单的认为它不需要判断都知道 (*__p)一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语句相关代码。

另外在linux内核中内存屏障也是基于它实现的include/asm/system.h中

# define barrier() _asm__volatile_("": : :"memory")
主要是保证程序的执行遵循顺序一致性。呵呵,有的时候你写代码的顺序,不一定是终执行的顺序,这个是处理器有关的。

Linux 源码例子

static inline char * strcpy(char * dest, const char *src)
{
    char *xdest = dest;
    __asm__ __volatile__
    ("1: \tmoeb %1@+, %0@+\n\t"   "jne 1b"  //这个冒号不是分隔符
    : "=a" (dest) , "=a" (stc)
    : "0"(dest), "1" (src)
     : "memory");
    return xdest;
}

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

https://blog.csdn.net/yi412/article/details/80846083

https://www.cnblogs.com/elnino/p/4313340.html