时间如流水

2008 8.21 Thu
     12
3456789
10111213141516
17181920212223
24252627282930
31      
«» 2008 - 8 «»

查找文章

日志文章

2008年06月27日 12:11:56

内联汇编的简单解释

内联汇编的基本要素

{
int a=10, b;
asm ("movl %1, %%eax;



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


在上例中,我们使用汇编指令使 "b" 的值等于 "a"。请注意以下几点:

* "b" 是输出操作数,由 %0 引用,"a" 是输入操作数,由 %1 引用。
* "r" 是操作数的约束,它指定将变量 "a" 和 "b" 存储在寄存器中。请注意,输出操作数约束应该带有一个约束修饰符 "=",指定它是输出操作数。
* 要在 "asm" 内使用寄存器 %eax,%eax 的前面应该再加一个 %,换句话说就是 %%eax,因为 "asm" 使用 %0、%1 等来标识变量。任何带有一个 % 的数都看作是输入/输出操作数,而不认为是寄存器。
* 第三个冒号后的修饰寄存器 %eax 告诉将在 "asm" 中修改 GCC %eax 的值,这样 GCC 就不使用该寄存器存储任何其它的值。
* movl %1, %%eax 将 "a" 的值移到 %eax 中, movl %%eax, %0 将 %eax 的内容移到 "b" 中。
* 因为 "b" 被指定成输出操作数,因此当 "asm" 的执行完成后,它将反映出更新的值。换句话说,对 "asm" 内 "b" 所做的更改将在 "asm" 外反映出来。

现在让我们更详细的了解每一项的含义。



汇编程序模板

汇编程序模板是一组插入到 C 程序中的汇编指令(可以是单个指令,也可以是一组指令)。每条指令都应该由双引号括起,或者整组指令应该由双引号括起。每条指令还应该用一个定界符结尾。有效的定界符为新行 (\n) 和分号 (;)。 '\n' 后可以跟一个 tab(\t) 作为格式化符号,增加 GCC 在汇编文件中生成的指令的可读性。 指令通过数 %0、%1 等来引用 C 表达式(指定为操作数)。

如果希望确保编译器不会在 "asm" 内部优化指令,可以在 "asm" 后使用关键字 "volatile"。如果程序必须与 ANSI C 兼容,则应该使用 __asm__ 和 __volatile__,而不是 asm 和 volatile。







操作数

C 表达式用作 "asm" 内的汇编指令操作数。在汇编指令通过对 C 程序的 C 表达式进行操作来执行有意义的作业的情况下,操作数是内联汇编的主要特性。

每个操作数都由操作数约束字符串指定,后面跟用括弧括起的 C 表达式,例如:"constraint" (C expression)。操作数约束的主要功能是确定操作数的寻址方式。

可以在输入和输出部分中同时使用多个操作数。每个操作数由逗号分隔开。

在汇编程序模板内部,操作数由数字引用。如果总共有 n 个操作数(包括输入和输出),那么第一个输出操作数的编号为 0,逐项递增,最后那个输入操作数的编号为 n -1。总操作数的数目限制在 10,如果机器描述中任何指令模式中的最大操作数数目大于 10,则使用后者作为限制。







修饰寄存器列表

如果 "asm" 中的指令指的是硬件寄存器,可以告诉 GCC 我们将自己使用和修改它们。这样,GCC 就不会假设它装入到这些寄存器中的值是有效值。通常不需要将输入和输出寄存器列为 clobbered,因为 GCC 知道 "asm" 使用它们(因为它们被明确指定为约束)。不过,如果指令使用任何其它的寄存器,无论是明确的还是隐含的(寄存器不在输入约束列表中出现,也不在输出约束列表中出现),寄存器都必须被指定为修饰列表。修饰寄存器列在第三个冒号之后,其名称被指定为字符串。

至于关键字,如果指令以某些不可预知且不明确的方式修改了内存,则可能将 "memory" 关键字添加到修饰寄存器列表中。这样就告诉 GCC 不要在不同指令之间将内存值高速缓存在寄存器中。







操作数约束

前面提到过,"asm" 中的每个操作数都应该由操作数约束字符串描述,后面跟用括弧括起的 C 表达式。操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:

* 是否允许操作数位于寄存器中,以及它可以包括在哪些种类的寄存器中
* 操作数是否可以是内存引用,以及在这种情况下使用哪些种类的地址
* 操作数是否可以是立即数

约束还要求两个操作数匹配。







常用约束

在可用的操作数约束中,只有一小部分是常用的;下面列出了这些约束以及简要描述。有关操作数约束的完整列表,请参考 GCC 和 GAS 手册。

寄存器操作数约束 (r)
使用这种约束指定操作数时,它们存储在通用寄存器中。请看下例:



asm ("movl %%cr3, %0\n" :"=r"(cr3val));


这里,变量 cr3val 保存在寄存器中,%cr3 的值复制到寄存器上,cr3val 的值从该寄存器更新到内存中。指定 "r" 约束时, GCC 可以将变量 cr3val 保存在任何可用的 GPR 中。要指定寄存器,必须通过使用特定的寄存器约束直接指定寄存器名。



a %eax

b %ebx

c %ecx

d %edx

S %esi

D %edi


内存操作数约束 (m)
当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,这与寄存器约束正好相反,后者先将值存储在要修改的寄存器中,然后将它写回内存位置中。但寄存器约束通常只在对于指令来说它们是绝对必需的,或者它们可以大大提高进程速度时使用。当需要在 "asm" 内部更新 C 变量,而您又确实不希望使用寄存器来保存其值时,使用内存约束最为有效。例如,idtr 的值存储在内存位置 loc 中:



("sidt %0\n" : :"m"(loc));



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



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


在匹配约束的示例中,寄存器 %eax 既用作输入变量,也用作输出变量。将 var 输入读取到 %eax,增加后将更新的 %eax 再次存储在 var 中。这里的 "0" 指定第 0 个输出变量相同的约束。即,它指定 var 的输出实例只应该存储在 %eax 中。该约束可以用于以下情况:

* 输入从变量中读取,或者变量被修改后,修改写回到同一变量中
* 不需要将输入操作数和输出操作数的实例分开

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







一般内联汇编用法示例

以下示例通过各种不同的操作数约束说明了用法。有如此多的约束以至于无法将它们一一列出,这里只列出了最经常使用的那些约束类型。

"asm" 和寄存器约束 "r" 让我们先看一下使用寄存器约束 r 的 "asm"。我们的示例显示了 GCC 如何分配寄存器,以及它如何更新输出变量的值。

int main(void)
{
int x = 10, y;

asm ("movl %1, %%eax;


"movl %%eax, %0;"
:"=r"(y) /* y is output operand */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}



在该例中,x 的值复制为 "asm" 中的 y。x 和 y 都通过存储在寄存器中传递给 "asm"。为该例生成的汇编代码如下:



main:




pushl %ebp




movl %esp,%ebp




subl $8,%esp




movl $10,-4(%ebp)




movl -4(%ebp),%edx /* x=10 is stored in %edx */
#APP /* asm starts here */




movl %edx, %eax /* x is moved to %eax */




movl %eax, %edx /* y is allocated in edx and updated */

#NO_APP /* asm ends here */




movl %edx,-8(%ebp) /* value of y in stack is updated with

the value in %edx */


当使用 "r" 约束时,GCC 在这里可以自由分配任何寄存器。在我们的示例中,它选择 %edx 来存储 x。在读取了 %edx 中 x 的值后,它为 y 也分配了相同的寄存器。

因为 y 是在输出操作数部分中指定的,所以 %edx 中更新的值存储在 -8(%ebp),堆栈上 y 的位置中。如果 y 是在输入部分中指定的,那么即使它在 y 的临时寄存器存储值 (%edx) 中被更新,堆栈上 y 的值也不会更新。

因为 %eax 是在修饰列表中指定的,GCC 不在任何其它地方使用它来存储数据。

输入 x 和输出 y 都分配在同一个 %edx 寄存器中,假设输入在输出产生之前被消耗。请注意,如果您有许多指令,就不是这种情况了。要确保输入和输出分配到不同的寄存器中,可以指定 & 约束修饰符。下面是添加了约束修饰符的示例。



int main(void)
{
int x = 10, y;

asm ("movl %1, %%eax;


"movl %%eax, %0;"
:"=&r"(y) /* y is output operand, note the

& constraint modifier. */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}


以下是为该示例生成的汇编代码,从中可以明显地看出 x 和 y 存储在 "asm" 中不同的寄存器中。



main:




pushl %ebp




movl %esp,%ebp




subl $8,%esp




movl $10,-4(%ebp)




movl -4(%ebp),%ecx /* x, the input is in %ecx */
#APP
movl %ecx, %eax
movl %eax, %edx /* y, the output is in %edx */

#NO_APP




movl %edx,-8(%ebp)









特定寄存器约束的使用

现在让我们看一下如何将个别寄存器作为操作数的约束指定。在下面的示例中,cpuid 指令采用 %eax 寄存器中的输入,然后在四个寄存器中给出输出:%eax、%ebx、%ecx、%edx。对 cpuid 的输入(变量 "op")传递到 "asm" 的 eax 寄存器中,因为 cpuid 希望它这样做。在输出中使用 a、b、c 和 d 约束,分别收集四个寄存器中的值。



asm ("cpuid"








: "=a" (_eax),









"=b" (_ebx),









"=c" (_ecx),









"=d" (_edx)








: "a" (op));



在下面可以看到为它生成的汇编代码(假设 _eax、_ebx 等... 变量都存储在堆栈上):




movl -20(%ebp),%eax /* store 'op' in %eax -- input */
#APP




cpuid
#NO_APP




movl %eax,-4(%ebp) /* store %eax in _eax -- output */




movl %ebx,-8(%ebp) /* store other registers in




movl %ecx,-12(%ebp)
respective output variables */





movl %edx,-16(%ebp)


strcpy 函数可以通过以下方式使用 "S" 和 "D" 约束来实现:



asm ("cld\n



rep\n



movsb"



: /* no input */



:"S"(src), "D"(dst), "c"(count));


通过使用 "S" 约束将源指针 src 放入 %esi 中,使用 "D" 约束将目的指针 dst 放入 %edi 中。因为 rep 前缀需要 count 值,所以将它放入 %ecx 中。

在下面可以看到另一个约束,它使用两个寄存器 %eax 和 %edx 将两个 32 位的值合并在一起,然后生成一个64 位的值:


#define rdtscll(val) \


__asm__ __volatile__ ("rdtsc" : "=A" (val))

The generated assembly looks like this (if val has a 64 bit memory space).

#APP




rdtsc
#NO_APP




movl %eax,-8(%ebp) /* As a result of A constraint




movl %edx,-4(%ebp)
%eax and %edx serve as outputs */

Note here that the values in %edx:%eax serve as 64 bit output.









使用匹配约束

在下面将看到系统调用的代码,它有四个参数:



#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \




: "=a" (__res) \




: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \





"d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}


在上例中,通过使用 b、c、d 和 S 约束将系统调用的四个自变量放入 %ebx、%ecx、%edx 和 %esi 中。请注意,在输出中使用了 "=a" 约束,这样,位于 %eax 中的系统调用的返回值就被放入变量 __res 中。通过将匹配约束 "0" 用作输入部分中第一个操作数约束,syscall 号 __NR_##name 被放入 %eax 中,并用作对系统调用的输入。这样,这里的 %eax 既可以用作输入寄存器,又可以用作输出寄存器。没有其它寄存器用于这个目的。另请注意,输入(syscall 号)在产生输出(syscall 的返回值)之前被消耗(使用)。







内存操作数约束的使用

请考虑下面的原子递减操作:



__asm__ __volatile__(








"lock; decl %0"








:"=m" (counter)








:"m" (counter));


为它生成的汇编类似于:



#APP
lock
decl -24(%ebp) /* counter is modified on its memory location */
#NO_APP.


您可能考虑在这里为 counter 使用寄存器约束。如果这样做,counter 的值必须先复制到寄存器,递减,然后对其内存更新。但这样您会无法理解锁定和原子性的全部意图,这些明确显示了使用内存约束的必要性。







使用修饰寄存器

请考虑内存拷贝的基本实现。

asm ("movl $count, %%ecx;



up: lodsl;



stosl;



loop up;"
: /* no output */
:"S"(src), "D"(dst) /* input */
:"%ecx", "%eax" ); /* clobbered list */


当 lodsl 修改 %eax 时,lodsl 和 stosl 指令隐含地使用它。%ecx 寄存器明确装入 count。但 GCC 在我们通知它以前是不知道这些的,我们是通过将 %eax 和 %ecx 包括在修饰寄存器集中来通知 GCC 的。在完成这一步之前,GCC 假设 % eax 和 %ecx 是自由的,它可能决定将它们用作存储其它的数据。请注意,%esi 和 %edi 由 "asm" 使用,它们不在修饰列表中。这是因为已经声明 "asm" 将在输入操作数列表中使用它们。这里最低限度是,如果在 "asm" 内部使用寄存器(无论是明确还是隐含地),既不出现在输入操作数列表中,也不出现在输出操作数列表中,必须将它列为修饰寄存器。







结束语

总的来说,内联汇编非常巨大,它提供的许多特性我们甚至在这里根本没有涉及到。但如果掌握了本文描述的基本材料,您应该可以开始对自己的内联汇编进行编码了。







参考资料

* 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

* 请参考 Using and Porting the GNU Compiler Collection (GCC)手册。

* 请参考 GNU Assembler (GAS)手册。

* 仔细阅读 Brennan's Guide to Inline Assembly。








关于作者



Bharata B. Rao 拥有印度 Mysore 大学的电子和通信工程的学士学位。他从 1999 年就开始为 IBM Global Services, India 工作了。他是 IBM Linux 技术中心的成员之一,他在该中心中主要从事 Linux RAS(可靠性、可用性和适用性)的研究。他感兴趣的其它领域包括操作系统本质和处理器体系结构。可以通过 rbharata@in.ibm.com 与他联系。








对本文的评价
太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?

albcamus 回复于:2006-04-17 09:16:17

AT&T x86 asm 语法

创建时间:2001-04-09
文章属性:翻译
文章提交:e4gle (e4gle_at_hackermail.com)

AT&T x86 asm 语法
译:el8<el8@m4in.org>,alert7<alert7@m4in.org>
from m4in security teams(www.m4in.org)

DJGPP 使用AT&T格式的汇编语法。和一般的intel格式的语法有点不同。主要不同点如下:

AT&T 语法颠倒了源和目的操作数的位置, 目的操作数在源操作数之后。寄存器操作数要有个%的前缀, 立即数操作数要有个$符号的前缀。存储器操作数的大小取决于操作码的最后一个字符。 它们是b (8-bit), w (16-bit), 和 l (32-bit).
这里有一些例子。 左边部分是intel指令格式,右边是at&t格式。
movw %bx, %ax // mov ax, bx
xorl %eax, %eax // xor eax, eax
movw $1, %ax // mov ax,1
movb X, %ah // mov ah, byte ptr X
movw X, %ax // mov ax, word ptr X
movl X, %eax // mov eax, X
大部分操作指令,at%t和intel都是差不多的,除了这些:
movsSD // movsx
movzSD // movz

S和D分辨代表源和目的操作数后缀。
movswl %ax, %ecx // movsx ecx, ax
cbtw // cbw
cwtl // cwde
cwtd // cwd
cltd // cdq
lcall $S,$O // call far S:O
ljmp $S,$O // jump far S:O
lret $V // ret far V
操作嘛前缀不能与他们作用的指令写在同一行。 例如, rep 和stosd应该是两个相互独立的指令, 存储器的情况也有一点不同。通常intel格式的如下:

section:[base + index*scale + disp]

被写成:

section:disp(base, index, scale)

这里有些例子:

movl 4(%ebp), %eax // mov eax, [ebp+4])
addl (%eax,%eax,4), %ecx // add ecx, [eax + eax*4])
movb $4, %fs:(%eax) // mov fs:eax, 4)
movl _array(,%eax,4), %eax // mov eax, [4*eax + array])
movw _array(%ebx,%eax,4), %cx // mov cx, [ebx + 4*eax + array])

Jump 指令通常是个短跳转。 可是, 下面这些指令都是只能在一个字节的范围内跳转: jcxz, jecxz, loop, loopz, loope, loopnz 和loopne。象在线文档所说的那样,一个jcxz foo可以扩展成以下工作:
jcxz cx_zero
jmp cx_nonzero
cx_zero:
jmp foo
cx_nonzero:
文档也注意到了mul和imul指令。 扩展的乘法指令只用一个操作数,例如, imul $ebx, $ebx将不会把结果放入edx:eax。使用imul %ebx中的单操作数来获得扩展结果。


--------------------------------------------------------------------------------

Inline Asm
我将首先开始inline asm, 因为似乎关于这方面的疑问非常多。这是最基本的语法了, 就象在线帮助信息中描述的:
__asm__(asm statements : outputs : inputs : registers-modified);

这四个字段的含义是:

asm statements - AT&T 的结构, 每新行都是分开的。
outputs - 修饰符一定要用引号引起来, 用逗号分隔
inputs - 修饰符一定要用引号引起来, 用逗号分隔
registers-modified - 名字用逗号分隔
一个小小的例子:
__asm__("
pushl %eax\n
movl $1, %eax\n
popl %eax"
);
假如你不用到特别的输入输出变量或者修改任何寄存器的值,一般来说是不会使用到其他的三个字段的,
让我们来分析一下输入变量。

int i = 0;

__asm__("
pushl %%eax\n
movl %0, %%eax\n
addl $1, %%eax\n
movl %%eax, %0\n
popl %%eax"
:
: "g" (i)
); // increment i
不要为上面的代码所困扰! 我将尽力来解释它。我们想让输入变量i加1,我们没有任何输出变量, 也没有改变寄存器值(我们保存了eax值)。因此,第二个和最后一个字段是空的。 因为指定了输入字段, 我们仍需要保留一个空的输出字段, 但是没有最后一个字段, 因为它没被使用。在两个空冒号之间留下一个新行或者至少一个空格。

下面让我们来看看输入字段。 附加描述符可以修正指令来让你给定的编译器来正确处理这些变量。他们一般被附上双引号。那么这个"g"是用来做什么的呢? 只要是合法的汇编指令,"g"就让编译器决定该在哪里加载i的值。一般来说,你的大部分输入变量都可以被赋予 "g", 让编译器决定如何去加载它们 (gcc甚至可以优化它们!)。 其他描述符使用"r" (加载到任何可用的寄存器去), "a" (ax/eax), "b" (bx/ebx), "c" (cx/ecx), "d" (dx/edx), "D" (di/edi), "S" (si/esi), 等等。

我们将要提到一个在asm代码里面的如%0的输入变量。如果我们有两个输入, 他们会一个是%0一个是%1, 在输入段里按顺序排列 (如下一个例子)。假如N个输入变量且没有输出变量, 从%0 到%N-1将和输入字段里的变量相对应, 按顺序排列。

如果任何的输入, 输出, 寄存器修改字段被使用, 汇编代码里的寄存器名必须用两个%来代替一个%。对应于第一个没有使用最后三个字段的例子。

让我们看看两个输入变量且引入了"volatile"的例子:

int i=0, j=1;
__asm__ __volatile__("
pushl %%eax\n
movl %0, %%eax\n
addl %1, %%eax\n
movl %%eax, %0\n
popl %%eax"
:
: "g" (i), "g" (j)
); // increment i by j
Okay, 现在我们已经有了两个输入变量了。没问题了, 我们只需要记住%0对应第一个输入变量(在这个例子中是i), %1对应在i后面的列出的j。
Oh yeah, 这个volatile到底是什么意思呢? 它防止你的编译器修改你的汇编代码,就是不进行优化(纪录, 删除, 结合,等等优化手段。), 不改变代码原样来汇编它们。建议一般情况下使用volatile选项。

让我们来看看输出字段:

int i=0;
__asm__ __volatile__("
pushl %%eax\n
movl $1, %%eax\n
movl %%eax, %0\n
popl %%eax"
: "=g" (i)
); // assign 1 to i
这看起来非常象我们前面提到的输入字段的例子; 确实也没有很大的不同。所有的输出修饰符前面都应该加上=字符,他们同样在汇编代码里面用%0到 %N-1来表示, 在输出字段按顺序排列。你一定会问如果同时有输入和输出字段会怎么排序的呢? 好,下面一个例子就是让大家知道如何同时处理输入输出字段的。
int i=0, j=1, k=0;
__asm__ __volatile__("
pushl %%eax\n
movl %1, %%eax\n
addl %2, %%eax\n
movl %%eax, %0\n
popl %%eax"
: "=g" (k)
: "g" (i), "g" (j)
); // k = i + j
Okay, 唯一个不清楚的地方就是汇编代码中的变量的个数。我马上来解释一下。
当同时使用输入字段和输出字段的时候:

%0 ... %K 是输出变量

%K+1 ... %N 是输入变量

在我们的例子中, %0 对应k, %1 对应i, %2对应j。很简单,是吧?

到现在为止我们都没有使用最后一个字段(registers-modified)。如果我们要在我们的汇编代码里使用任何寄存器, 我们要明确的用push和pop指令来保存它们, 或者列到最后一个字段里面让gcc来处理它们。

这是前面的一个例子, 没有明确的保留和存贮eax。

int i=0, j=1, k=0;
__asm__ __volatile__("
pushl %%eax\n /*译者注:好像原文说的有点问题,明明是保存了eax的值,:(*/
movl %1, %%eax\n
addl %2, %%eax\n
movl %%eax, %0\n
popl %%eax"
: "=g" (k)
: "g" (i), "g" (j)
: "ax", "memory"
); // k = i + j
我们让gcc来保存和存贮eax, 如果必要的话。一个16-bit寄存器名代表了32-, 16-或8-bit寄存器。 如果我们要改写内存 (写入一个变量等。), 建议在register-modified字段里面来指定"memroy"修饰符。这意味着除了第一个例子我们都应该加上这个修饰符, 但是直到现在我才提出来, 是为了更简单易懂。

在你的内联汇编里面定位标号应该使用b或f来作为终止符, 尤其是向后向前的跳转。(译者注:b代表向后跳转,f代表向前跳转)

For example,

__asm__ __volatile__("
0:\n
...
jmp 0b\n
...
jmp 1f\n
...
1:\n
...
);
这里有个用c代码和内联汇编代码混合写的跳转程序的例子(thanks to Srikanth B.R for this tip).

void MyFunction( int x, int y )
{
__asm__( "Start:" );
__asm__( ...do some comparison... );
__asm__( "jl Label_1" );

CallFunction( &x, &y );
__asm__("jmp Start");

Label_1:
return;
}

--------------------------------------------------------------------------------

External Asm
Blah... Okay fine. Here's a clue: Get some of your C/C++ files, 且用gcc -S file.c来编译。 然后查看file.S文件。基本结构如下:
.file "myasm.S"

.data
somedata: .word 0
...

.text
.globl __myasmfunc
__myasmfunc:
...
ret
Macros, macros! 头文件libc/asmdefs.h便于你写asm。 在你的汇编代码最前面包含此头文件然后就可以使用宏了。一个例子: myasm.S:
#include <libc/asmdefs.h>

.file "myasm.S"

.data
.align 2
somedata: .word 0
...

.text
.align 4
FUNC(__MyExternalAsmFunc)
ENTER
movl ARG1, %eax
...
jmp mylabel
...
mylabel:
...
LEAVE

类别: 无分类 |  评论(0) |  浏览(2160) |  收藏
发表评论