GCC内联汇编基础

这篇文章阐述内联汇编的使用方法。显然,阅读这篇文章您需要具备X86汇编语言和C语言的基础知识。
Contents
1. 简介 3
2. 概要 3
3. GCC汇编格式。 3
1) 源操作数和目的操作数的方向 3
2) 寄存器命名 4
3) 立即数 4
4) 操作数大小 4
5) 内存操作数 4
4. 基本形式的内联汇编 4
5. 扩展形式的内联汇编 5
5.1 汇编模板 6
5.2 操作数 6
5.3 Clobber List 7
5.4 Volatile…? 8
6. 深入constraints 8
6.1 常用constraints 8
6.2 constraint修改标记 10
7.常用技巧 10
8.结束语 13
9. 参考文献 13
1. 简介
[主要是版权/反馈/勘误/感谢等信息。没有翻译。--译者注, 本文中方括号中的都是译者注]
2. 概要
我们现在学习GCC内联汇编,那么内联汇编到底是什么?
[我们首先先来看看内联函数有什么好处]
我们可以让编译器将函数代码插入到调用者代码中,指出函数在代码中具体什么位置被执行。这种函数就是内联函数。内联函数似乎很像一个宏?的确,他们之间有很多相似之处。
那么内联函数到底有什么好处呢?
内联函数降低了函数调用的开销。[不仅仅节省堆栈] 如果某些函数调用的实参相同,那么返回值一定是相同的,这就可能给编译器留下了简化的空间。因为返回值相同了就不必把内联函数的代码插入到调用者的代码中 [直接用这个返回值替换就好了]。这样可以减少代码量,视不同的情况而定。声明一个函数是内联函数,使用关键字 inline。
现在我们回到内联汇编上来。内联汇编就是一些汇编语句写成的内联函数。它方便,快速,对系统编程非常有用。我们主要目标是研究GCC内联函数的基础格式和使用方法。声明一个内联汇编函数,我们使用关键字 asm。
内联汇编的重要性首先体现在它的操作C语言变量和输出值到C语言变量的能力。由于这些特性,内联汇编常被用作汇编指令和调用它的C程序之间的接口。
3. GCC汇编格式
GCC (GNU Compiler for Linux) 使用AT&T UNIX汇编语法.这里我们将用AT&T汇编格式来写代码。如果你不熟悉AT&T汇编语法也没有关系,下面将有介绍。AT&T和 Intel汇编语法有很多的不同之处。我将给出主要的不同点。
1) 源操作数和目的操作数的方向
AT&T和Intel汇编语法相反。Intel语法中第一个操作数作为目的操作数,第二个操作数作为源操作数。相反,在AT&T语法中,第一个操作数是源操作数,第二个是目的操作数,例如:
Intel语法: "OP-code dst src"
AT&T语法: "Op-code src dst"
2) 寄存器命名
[在AT&T语法中] 寄存器名字加上%前缀,例如,如果要使用eax, 写作: %eax.
3) 立即数
AT&T 语法中,立即数以'$'符号作为前缀。静态C变量前也要加上'$'前缀。在Intel语法中,16进制的常数加上'h'后缀,但是在AT&T中, 常量前要加上'0x'。 对于一个16进制常数(在AT&T中),首先以$开头接着是0x,最后是常数。
4) 操作数大小
在 AT&T语法中,操作数占内存大小决定于汇编命令操作符的最后一个字符的内容。 操作符以'b', 'w'和 'l'为后缀指明内存访问长度是 byte(8-bit), word(16-bit)还是long(32-bit). 而Intel语法在操作数前加上'byte ptr', 'word ptr'和'dword ptr'的内存操作数(这个操作数不是汇编命令操作符)来达到相同目的.
因此, Intel "mov al, byte ptr foo" 用AT&T语法就是 :"movb foo, %al"
5) 内存操作数
在Intel的语法中,基址寄存器用'['和']'扩起来,但是在AT&T中改用'('和')'。 此外,在Intel语法中一个间接内存寻址:
section:[base + index * scale + disp],在AT&T中则为:
section:disp(base, index, scale)
总是需要记住的一点就是,当一个常数被用作disp或者scale时,就不用加'$'前缀。
现在我们已经提到了AT&T和Intel语法的一些主要不同点。 我只是提到了一小部分。全部内容可以参考GNU汇编文档。为了更好理解这些不同,请看下面的例子:
Intel Code                              AT&T Code
mov eax,1                               movl $1, %eax
mov ebx,0ffh                            movl $0x0ff,%ebx,
int 80h                                 int $0x80
mov ebx,eax                             movl %eax,%ebx
mov eax,[ecx]                           movl (%ecx),%eax
mov eax,[ecx+3]                         movl 3(ecx),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
4. 基本形式的内联汇编
基本形式的内联汇编格式非常直观:
asm(“assembly code”);
例如:
asm("movl %ecx, %eax"); /* 把 ecx 内容移动到 eax */
__asm__("movb %bh , (%eax)"); /* 把bh中一个字节的内容移动到eax 指向的内存 */
你可能注意到了这里使用了 asm 和 __asm__关键字.二者皆可。这样如果asm关键字和程序其他变量有冲突就可以使用__asm__了。如果有超过一行的的指令,每行要加上双引号,并且后面加上/n/t. 这是因为GCC将每行指令作为一个字符串传给as(GAS),使用换行和TAB可以给汇编器传送正确的格式化好的代码行。
例如:
__asm__ ("movl %eax, %ebx/n/t"
"movl $56, %esi/n/t"
"movl %ecx, $label(%edx,%ebx,$4)/n/t"
"movb %ah, (%ebx)");
如果我们的代码涉及到一些寄存器(例如改变了其内容)并且从汇编代码返回后并没有修复这些改变,一些意想不到的情况可能发生。因为GCC不知道你已经将寄存器内容改了,这将给我们带来麻烦,尤其在编译器作了一些优化的情况下。如果不告诉GCC,编译器将认为寄存器中事实上已经被改掉了的内容没有被改过,程序将当作它没有被改过而继续执行。我们能做的就是不要使用这些带来其他附加影响的语句或者当我们退出的时候还原这些内容,否则只有等待程序崩溃了。这里提到的这种情况就是我们将要在下节中阐述的扩展形式的内联汇编。
5. 扩展形式的内联汇编
前面介绍的基础形式的内联汇编方法只涉及到嵌入汇编指令。在高级形式中,我们将可以指定操作数,它允许我们指定输入输出寄存器[内联函数使用这些寄存器作为存储输入输出变量]和程序中涉及到的clobbered寄存器列表[clobbered registers:内联汇编程序可能要改变其内容的寄存器]。也并不是一定要要显式指明使用具体的寄存器,我们也可以把它留给GCC去选择,这样GCC 还可能更好的进行优化处理。高级内联汇编的基本格式如下:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
其中assembler template包含汇编指令部分。括号中每个操作数用C表达式常量串描述。不同部分之间用冒号分开。相同部分中的每个小部分用逗号分开。操作数多少被限定为10或者由机器决定的一个最大值[这句话翻译的不好,贴出原文: The total number of operands is limited to ten or to the maximum number of operands in any instruction pattern in the machine description, whichever is greater.]。
如果没有输出部分但是有输入部分,就必须在输出部分之前连续写两个冒号。
例如 :
asm ("cld/n/t"
"rep/n/t"
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
现在我们来分析上面的代码的功能。上面代码循环count次把fill_value的值到填充到edi寄存器指定的内存位置。并且告诉GCC寄存器eax[这里应该是ecx]和edi中内容可能已经被改变了。
为了有一个更清晰的理解,我们再来看一个例子:
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
上面代码所做的就是用汇编代码把a的值赋给b。值得注意的几点有:
1) “b”是输出操作数,用%0来访问,”a”是输入操作数,用%1来访问。
2) “r” 是一个constraint, 关于constraint后面有详细的介绍。这里我们只要记住这里constraint ”r”让GCC自己选择一个寄存器去存储变量a。输出部分的constraint前必须要有个 ”=”,用来说明是一个这是一个输出操作数,并且只写。
3) 你可能看到有的寄存器名字前面写了两个%,这是用来帮助GCC区分操作数和寄存器。操作数只需要一个%前缀。
4) 在第三个冒号后面的clobbered register部分, %eax 说明在内联汇编代码中将要改变eax中的内容,GCC不要用他存储其他值。
当这段代码执行结束后,”b”的值将会被改掉,因为它被指定作为输出操作数。换句话说,在”asm”内部对b的改动将影响到asm外面.
下面我们将对各个部分分别进行详细的讨论:
5.1 汇编模板
汇编模板部分包含嵌入到C程序中的汇编指令。格式如下:
每条指令放在一个双引号内,或者将所有的指令都放着一个双引号内。每条指令都要包含一个分隔符。合法的分隔符是换行符(/n)或者分号。用换行符的时候通常后面放一个制表符”/t”。我们已经知道为什么使用换行符+制表符了[前面部分有解释]。其中,访问C操作数用%0,%1…等等。
5.2 操作数
C语言表达式 [大多情况是C变量] 将作为”asm”内部使用的操作数。每一个操作数都以双引号开始。对于输出操作数,还要写一个修改标志(=)。constraint和修改标志都放在双引号内。接下来部分就是C表达式了[放在括号内].举例来说:
标准形式如下:
"constraint" (C expression) [ 如: “=r”(result) ]
对于输出操作数还有一个修改标志(=)。 constraint主要用来指定操作数的寻址类型 (内存寻址或寄存器寻址),也用来指明使用哪个寄存器。
如果有多个操作数,之间用逗号分隔。
在汇编模板中,每个操作数都用数字引用[这些操作数],引用规则如下,如果总共有n个操作数(包括输入输出操作数),那么第一个输出操作引用数字为0,依次递增,然后最后一个操作数是n-1。关于最多操作数限制参见前面的小结。
输出操作数表达式必须是左值,输入操作数没有这个限制。注意这里可以使表达式[不仅仅限于变量]。高级汇编形式常用在当编译器不知道这个机器指令存在的时候。;-)如果输出表达式不能直接寻址(比如是bit-field), constraint就必须指定一个寄存器.这种情况下,GCC将使用寄存器作为asm的输出。然后保存这个寄存器的值到输出表达式中。
如上所述,一般输出操作数必须是只写的;GCC将认为在这条指令之前,保存在这种操作数中的值已经过期和不再需要了 。高级形式的asm也支持输入输出或者读写操作数。
现在我们来看一些例子,把一个数字乘以5使用汇编指令lea
asm( “leal (%1,%1,4), %0”
: ”=r” (five_times_x)
: “r” (x)
);
这里输入操作数是 ‘x’,不指定具体使用那个寄存器,GCC会自己选择输入输出的寄存器来操作。如果我们也可以让GCC把输入和输出寄存器限定同一个。只需要使用读写操作数,使用合适的constraint,看下具体方法:
asm(“lea (%0,%0,4),%0”
: “=r” (five_times_x)
: “0” (x)
);
上面使输入和输出操作数存在相同的寄存器中,我们不知道GCC具体使那个寄存器,但是我们也可以指定一个,像这样:
asm(“lea (%0,%0,4),%0”
: “=c” (five_times_x)
: “c” (x)
);
上面的三个例子中,我没有都没有在clobber list中放入任何寄存器的值,这是为什么?在前两个例子中,GCC决定使用那个寄存器并且自己知道哪儿改变了。

 

闻香止步 收藏于:http://www.diybl.com/course/6_system/linux/Linuxjs/20081010/149820.html

你可能感兴趣的:(GCC内联汇编基础)