Linux上的 GNU C 编译器 GCC ,使用 AT&T / UNIX 汇编语法
源操作数与目的操作数顺序
AT&T 语法的操作数方向和 Intel 语法的刚好相反。在Intel 语法中,第一操作数为目的操作数,第二操作数为源操作数,然而在 AT&T 语法中,第一操作数为源操作数,第二操作数为目的操作数。也就是说,
Intel 语法中的 "Op-code dst src" 变为 AT&T 语法中的 "Op-code src dst"(顺写)。
asm("movl %ecx %eax"); /* 将 ecx 寄存器的内容移至 eax */ __asm__("movb %bh (%eax)"); /* 将 bh 的一个字节数据 移至 eax 寄存器指向的内存 */
寄存器命名
% 前缀,使用eax: %eax.
立即数
AT&T 立即数以 "$" 为前缀。静态 "C" 变量也使用 "$" 前缀。在 Intel 语法中,十六进制常量以 "h" 为后缀,然而 AT&T 不使用这种语法,这里我们给常量添加前缀 "0x"。所以,对于十六进制,我们首先看到一个 "$",然后是 "0x",最后才是常量。
例如:mov $0x10,%eax
操作数长度大小
在 AT&T 语法中,存储器操作数的大小取决于操作码名字的最后一个字符。
操作码后缀 ’b’ 、’w’、’l’ 分别指明了字节(8位)、字(16位)、长型(32位)存储器引用。Intel 语法通过给存储器操作数添加 "byte ptr"、 "word ptr" 和 "dword ptr" 前缀来实现这一功能。
因此,Intel的 "mov al, byte ptr foo"
在 AT&T 语法中为 "movb foo, %al"。
寄存器寻址
在 Intel 语法中,基址寄存器包含在 "[" 和 "]" 中,
然而在 AT&T 中,它们变为 "(" 和 ")"。
另外,在 Intel 语法中, 间接内存引用为
"section:[base + index*scale + disp]",
在 AT&T中变为 "section:disp(base, index, scale)"。
需要牢记的一点是,当一个常量用于 disp 或 scale,不能添加 "$" 前缀。
(现在我们看到了 Intel 语法和 AT&T 语法之间的一些主要差别。我仅仅写了它们差别的一部分而已。关于更完整的信息,请参考 GNU 汇编文档。现在为了更好地理解,我们可以看一些示例。)
+------------------------------+------------------------------------+ | Intel Code | AT&T Code | +------------------------------+------------------------------------+ | mov eax,1 | movl $1,%eax | | mov ebx,0ffh | movl $0xff,%ebx | | int 80h | int $0x80 | | mov ebx, eax | movl %eax, %ebx | | mov eax,[ecx] | movl (%ecx),%eax | | mov eax,[ebx+3] | movl 3(%ebx),%eax | | mov eax,[ebx+20h] | movl 0x20(%ebx),%eax | | add eax,[ebx+ecx*2h] | addl (%ebx,%ecx,0x2),%eax | | lea eax,[ebx+ecx] | leal (%ebx,%ecx),%eax | | sub eax,[ebx+ecx*4h-20h] | subl -0x20(%ebx,%ecx,0x4),%eax | +------------------------------+------------------------------------+movl (%ecx),%eax:
以ecx中的内容为基地址,取所在地址中的内容
基本汇编
基本内联汇编的格式非常直接了当。它的基本格式为:
asm("汇编代码");
__asm__ ("movl %eax, %ebx/n/t"
"movl $56, %esi/n/t"
"movl %ecx, $label(%edx,%ebx,$4)/n/t"
"movb %ah, (%ebx)");
如果在代码中,我们涉及到一些寄存器(即改变其内容),但在没有恢复这些变化的情况下从汇编中返回,这将会导致一些意想不到的事情。这是因为 GCC 并不知道寄存器内容的变化,这会导致问题,特别是当编译器做了某些优化。所以扩展汇编中,有一些扩展功能
扩展汇编
asm ( 汇编程序模板
: 输出操作数 /* 可选的 */
: 输入操作数 /* 可选的 */
: 修饰寄存器列表 /* 可选的 */
);汇编程序模板由汇编指令组成。
每一个操作数由一个操作数约束字符串所描述,其后紧接一个括弧括起的 C 表达式。
冒号用于将汇编程序模板和第一个输出操作数分开,另一个(冒号)用于将最后一个输出操作数和第一个输入操作数分开。
逗号用于分离每一个组内的操作数。总操作数的数目限制在 10 个,或者机器描述中的任何指令格式中的最大操作数数目,以较大者为准。
__asm__ __volatile__("InSTructiON List" : Output : Input : Clobber/Modify);
asm("汇编语句"
:输出操作数
:输入操作数
:会被修改的寄存器);
说明:
1. _asm_ 是GCC关键字asm的宏定义:#define __asm__ asm
Volatile 通知编译器不要去做优化
2. 输出操作数 ”= “表示这是输出寄存器
3. 输入操作数 汇编葱须规定把 输出和输入
寄存器(操作数) 按顺序统一编号(??,应该是操作数进行编号),从输出操作数序列,从左到右,从上倒下,以0%开始,分别记为 %0、1%、,,,%9 (总共大概是10个操作数)4. 会被修改的寄存器,可以写:"%eax", "%edx", "memory"等
示例
asm ("cld/n/t"
"rep/n/t"
"stosl"
: /* 无输出寄存器 */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
现在来看看这段代码是干什么的?以上的内联汇编是将 "fill_value" 值连续 "count" 次拷贝到寄存器 "edi" 所指位置(注:每执行 stosl 一次,寄存器 edi 的值会递增或递减,这取决于是否设置了 direction 标志,因此以上代码实则初始化一个内存块)。 它也告诉 gcc 寄存器 "ecx" 和 "edi" 一直无效。
为了更加清晰地说明,让我们再看一个示例。
使用汇编指令使 ’b’ 变量的值等于 ’a’ 变量的值:
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* 输出 */
:"r"(a) /* 输入 */
:"%eax" /* 修饰寄存器 */
);
- "b" 为输出操作数,用 %0 引用,并且 "a" 为输入操作数,用 %1 引用。
- "r" 为操作数约束。之后我们会更详细地了解约束(字符串)。目前,"r" 告诉 GCC 可以使用任一寄存器存储操作数。输出操作数约束应该有一个约束修饰符 "=" 。这修饰符表明它是一个只读的输出操作数。
- 寄存器名字以两个 % 为前缀。这有利于 GCC 区分操作数和寄存器。操作数以一个 % 为前缀。
- 第三个冒号之后的修饰寄存器 %eax 用于告诉 GCC %eax 的值将会在 "asm" 内部被修改,所以 GCC 将不会使用此寄存器存储任何其他值。
当 “asm” 执行完毕, "b" 变量会映射到更新的值,因为它被指定为输出操作数。换句话说, “asm” 内 "b" 变量的修改应该会被映射到 “asm” 外部。
现在,我们可以更详细地看看每一个域
模板:
__asm__("汇编语句1;
汇编语句2;"
:output
:input
:Clobber/Modify
)
其格式为:
a-1)每条指令用双引号圈起,或者整个指令组用双引号圈起
a-2)同时每条指令应以分界符结尾。有效的分界符有换行符("/n")和分号(";")。"/n" 可以紧随一个制表符("/t")
b-1)汇编代码,输出输入操作数,用冒号隔开,最后一个隔开输入操作数和 可能影响的寄存器列表
关于约束
常用约束/strong>
1.寄存器约束
当使用这种约束指定操作数时,它们存储在通用寄存器(GPR)中。请看下面示例:
asm ("movl %%eax, %0/n" :"=r"(myval));
变量 myval 保存在寄存器中,寄存器 eax 的值被复制到该寄存器中,并且 myval 的值从寄存器更新到了内存。当指定 "r" 约束时, gcc 可以将变量保存在任何可用的 GPR 中。要指定寄存器,你必须使用特定寄存器约束直接地指定寄存器的名字。它们为:
+---+--------------------+
| 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 变量,而又不想使用寄存器去保存它的值,使用内存最为有效。例如,IDTR 寄存器的值存储于内存位置 loc 处:
asm("sidt %0/n" : :"m"(loc)); //同"m" memory
匹配(数字)约束
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。
- "I" : 范围从 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" : 指定 "a" 或 "d" 寄存器。这主要用于想要返回 64 位整形数,使用 "d" 寄存器保存最高有效位和 "a" 寄存器保存最低有效位。
约束修饰符
当使用约束时,对于更精确的控制超过了对约束作用的需求,GCC 给我们提供了约束修饰符。最常用的约束修饰符为:
- "=" : 意味着对于这条指令,操作数为只写的;旧值会被忽略并被输出数据所替换。
- "&" : 意味着这个操作数为一个早期改动的操作数,其在该指令完成前 通过使用输入操作数 被修改了。因此,这个操作数不可以位于一个被用作输出操作数或任何内存地址部分的寄存器。如果在旧值被写入之前它仅用作输入而已,一个输入操作数可以为一个早期改动操作数。
上述的约束列表和解释并不完整。示例可以让我们对内联汇编的用途和用法更好的理解。
常用约束
“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” ... 也即该操作数就是指定的那个操作数,例如“0”
“9” 去描述“%1”操作数,那么“%1”引用的其实就
是“%0”操作数,注意作为限定符字母的0-9 与
指令中的“%0”-“%9”的区别,前者描述操作数,
后者代表操作数。
& 该输出操作数不能使用和输入操作数相同的寄存器
意味着这个操作数为一个早期改动的操作数,其在该指令完成前 通过使用输入操作数 被修改
了,因此,这个操作数不可以位于一个被用作输出操作数或任何内存地址部分的寄存器。如果在旧值被写入之前它仅用作输入而已,一个输入操作数可以为一个早期改动操作数。
“=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)浮点数
“f” 浮点寄存器
“t” 第一个浮点寄存器
“u” 第二个浮点寄存器
“G” 标准的80387浮点常数
% 该操作数可以和下一个操作数交换位置 例如addl的两个操作数可以交换顺序
(当然两个操作数都不能是立即数)
# 部分注释,从该字符到其后的逗号之间所有字母被忽略
* 表示如果选用寄存器,则其后的字母被忽略
& 独占
GCC不得为任何Input操作表达式分配与此Output操作表达式相同的寄存器
int main(int __argc, char* __argv[])
{
int __in1 = 8, __in2 = 4, __out = 3;__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=a"(__out)
: "r" (__in1), "r" (__in2));return 0;
}此例中,%0对应的就是Output操作表达式,它被指定的寄存器是%eax,整个Instruction List的第一条指令popl %0,编译后就成为popl %eax,这时%eax的内容已经被修改,随后在Instruction List后,GCC会通过movl %eax, (address_of_out)这条指令将%eax的内容放置到Output变量__out中。对于本例中的两个Input操作表达式而言,它们的寄存器约束为"r",即要求GCC为其指定合适的寄存器,然后在Instruction List之前将__in1和__in2的内容放入被选出的寄存器中,如果它们中的一个选择了已经被__out指定的寄存器%eax,假如是__in1,那么GCC在Instruction List之前会插入指令movl (address_of_in1), %eax,那么随后popl %eax指令就修改了%eax的值,此时%eax中存放的已经不是Input变量__in1的值了,那么随后的movl %1, %%esi指令,将不会按照我们的本意——即将__in1的值放入%esi中——而是将__out的值放入%esi中了。
如果你为所有的 Input操作表达式指定了固定的寄存器,或使用内存/立即数约束,则此Output操作表达式使用&修饰没有任何意义
例子1
写一个两个数相加的程序:
int main(void)
{
int foo = 10, bar = 15;
__asm__ __volatile__("addl %%ebx,%%eax"
:"=a"(foo)
:"a"(foo), "b"(bar)
);
printf("foo+bar=%d/n", foo);
return 0;
}foo 存入 eax,bar 存入 ebx;
将eax中的结果写到foo内存
例子2:寄存器/变量上展示一些操作,并比较值
__asm__ __volatile__( "decl %0; sete %1"
: "=m" (my_var), "=q" (cond)
: "m" (my_var)
: "memory"
);这里,my_var 的值减 1 ,并且如果结果的值为 0,则变量 cond 置 1。
我们可以通过将指令 "lock;/n/t" 添加为汇编模板的第一条指令以增加原子性。
(i)my_var 是一个存储于内存的变量。
(ii)cond 位于寄存器 eax、ebx、ecx、edx 中的任何一个。约束 "=q" 保证了这一点。(iii)同时我们可以看到 memory 位于修饰寄存器列表中。也就是说,代码将改变内存中的内容。
例子3:系统调用
type name(type1 arg1,type2 arg2,type3 arg3) /
{ /
long __res; /
__asm__ volatile ( "int $0x80" /
: "=a" (__res) /
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), /
"d" ((long)(arg3))); /
__syscall_return(type,__res); /
}系统调用号位于 eax 中,其他参数位于 ebx、ecx、edx 中。最后 "int 0x80" 是一条用于执行系统调用的指令。返回值被存储于 eax 中。
Exit 是一个单一参数的系统调用,让我们看看它的代码看起来会是怎样
{
asm("movl $1,%%eax; /* SYS_exit is 1 */
xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */
int $0x80" /* Enter kernel mode */
);
}eax 保存调用号 1,ebx 保存0,同时通过 "int $0x80" ,这就是"exit(0)"的执行过程
例子4:
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__( "1:/tlodsb/n/t" //[esi]->al esi++
"stosb/n/t" //al->[edi] edi++
"testb %%al,%%al/n/t" //al&al al=0(即\0)循环退出
"jne 1b"
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
: "0" (src),"1" (dest)
: "memory");
return dest;
}源地址存放于 esi,目标地址存放于 edi,同时开始拷贝,当我们到达 0 时,拷贝完成
约束 "&S"、"&D"、"&a" 表明寄存器 esi、edi 和 eax 早期修饰寄存器(数据在使用输入操作数时,被改动)
"0" 使用 esi ,存入src "1" 使用edi ,存入 dest
汇编结束,相关寄存器写入 output 操作数,&约束修辞符,不起作用
看个简单的例子,足以说明前3点
asm("leal (%1, %1, 4), %0"
: "=r" (y)
: "0" (x)
);
"leal (%1, %1, 4), %0"
表示:汇编指令
: "=r" (y)
表示:"=" 输出,r,动态分配寄存器,寄存器exa输出到 y变量
: "0" (x)
表示:0,%0寄存器,也就是exa, x-->exa
对于说明中的第4点,现在说明下:
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希看GCC在编译时能够将这一点考虑进往。那么你就可以在Clobber/Modify域声明这些寄存器或内存。
这种情况一般发生在:
1. 一个寄存器出现在"Instruction List",
2. 但却不是由Input/Output操纵表达式所指定的,
3. 也不是在一些Input/Output操纵表达式使用"r"约束时由GCC 为其动态选择的,
4. 同时此寄存器被"Instruction List"中的指令修改,
5. 而这个寄存器只是供当前内联汇编临时使用的情况。
例如:
__asm__ ("mov R0, #0x34" : : : "R0");
寄存器R0出现在"Instruction List中",并且被mov指令修改,但却未被任何Input/Output操纵表达式指定,所以你需要在Clobber/Modify域指定"R0",以让GCC知道这一点。
更详尽的说明
由于你在Input/Output操纵表达式所指定的寄存器,或当你为一些Input/Output操纵表达式使用"r"约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。
但除此之外, GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。
所以假如你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify 中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。(我们所可以做的是使用那些没有副作用的指令,或者当我们退出时恢复这些寄存器,要不就等着程序崩溃吧)
我们参看内存被修改的情况,寄存器中的值没有及时更新(类似于 volatile 功能):
假如一个内联汇编语句的Clobber/Modify域存在"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语句相关代码。
案例1
static __inline__ unsigned long ffz(unsigned long word)
{
__asm__("bsfq %1,%0"
:"=r" (word)
:"r" (~word));
return word;
}