gcc中的内嵌汇编语言(Intel i386平台)
一.声明 虽然Linux的核心代码大部分是用C语言编写的,但是不可避免的其中 还是有一部分是用汇编语言写成的。有些汇编语言代码是直接写在汇 编源程序中的,特别是Linux的启动代码部分;还有一些则是利用gcc 的内嵌汇编语言嵌在C语言程序中的。这篇文章简单介绍了gcc中的内 嵌式汇编语言,主要想帮助那些才开始阅读Linux核心代码的朋友们 能够更快的入手。
写这篇文章的主要信息来源是GNU的两个info文件:as.info和 gcc.info,如果你觉得这篇文章中的介绍还不够详细的话,你可以查 阅这两个文件。当然,直接查阅这两个文件可以获得更加权威的信息。 如果你不想被这两篇文档中的一大堆信息搞迷糊的话,我建议你先阅 读一下这篇文章,然后在必要时再去查阅更权威的信息。
二.简介 在Linux的核心代码中,还是存在相当一部分的汇编语言代码。如果 你想顺利阅读Linux代码的话,你不可能绕过这一部分代码。在Linux 使用的汇编语言代码中,主要有两种格式:一种是直接写成汇编语言 源程序的形式,这一部分主要是一些Linux的启动代码;另一部分则 是利用gcc的内嵌式汇编语言语句asm嵌在Linux的C语言代码中的。这 篇文章主要是介绍第二种形式的汇编语言代码。
首先,我介绍一下as支持的汇编语言的语法格式。大家知道,我们现 在学习的汇编语言的格式主要是Intel风格的,而在Linux的核心代码 中使用的则是AT&T格式的汇编语言代码,应该说大部分人对这种格式 的汇编语言还不是很了解,所以我觉得有必要介绍一下。
接着,我主要介绍一下gcc的内嵌式汇编语言的格式。gcc的内嵌式汇 编语言提供了一种在C语言源程序中直接嵌入汇编指令的很好的办法, 既能够直接控制所形成的指令序列,又有着与C语言的良好接口,所 以在Linux代码中很多地方都使用了这一语句。
三.AT&T的汇编语言语法格式 我想我们大部分人对Intel格式的汇编语言都很了解了。但是,在 Linux核心代码中,所有的汇编语言指令都是用AT&T格式的汇编语 言书写的。这两种汇编语言在语法格式上有着很大的不同:
1.在AT&T的汇编语言中,用 '$ '前缀表示一个立即操作数;而在Intel 的格式中,立即操作数的表示不带任何前缀符。例如:下面两个语句 是完全相同的: *AT&T: pushl $4 *Intel: push 4
2.AT&T和Intel的汇编语言格式中,源操作数和目标操作数的位置正 好相反。Intel的汇编语言中,目标操作数在源操作数的左边;而在 AT&T的汇编语言中,目标操作数则在源操作数的右边。例如: *AT&T : addl $4,%eax *Intel: add eax,4
3.在AT&T的汇编语言中,操作数的字长是由操作码助记符的最后一个 字母决定的,后缀 'b '、 'w '、 'l '分别表示操作数的字长为8比特(字 节,byte),16比特(字,word)和32比特(长字,long),而 Intel格式中操作数的字长是用“word ptr”或者“byte ptr”等前 缀来表示的。例如: *AT&T: movb FOO,%al *Intel: mov al,byte ptr FOO
4.在AT&T汇编指令中,直接远跳转/调用的指令格式是“lcall/ljmp $SECTION,$OFFSET”,同样,远程返回的指令是“lret $STACK-ADJUST”;而在Intel格式中,相应的指令分别为“call/jmp far SECTION:OFFSET”和“ret far STACK-ADJUST”。
①AT&T汇编指令操作助记符命名规则 AT&T汇编语言中,操作码助记符的后缀字符指定了该指令中操作数的 字长。后缀字母 'b '、 'w '、 'l '分别表示字长为8比特(字节,byte), 16比特(字,word)和32比特(长字,long)的操作数。如果助记符 中没有指定字长后缀并且该指令中没有内存操作数,汇编程序 'as '会 根据指令中指定的寄存器操作数补上相应的后缀字符。所以,下面的 两个指令具有相同的效果(这只是GNU的汇编程序as的一个特性,AT&T 的Unix汇编程序将没有字长后缀的指令的操作数字长假设为32比特):
mov %ax,%bx
movw %ax,%bx
AT&T中几乎所有的操作助记符与Intel格式中的助记符同名,仅有一 小部分例外。操作数扩展指令就是例外之一。在AT&T汇编指令中,操 作数扩展指令有两个后缀:一个指定源操作数的字长,另一个指定目 标操作数的字长。AT&T的符号扩展指令的基本助记符为 'movs ',零扩 展指令的基本助记符为 'movz '(相应的Intel指令为 'movsx '和 'movzx ')。因此, 'movsbl %al,%edx '表示对寄存器al中的字节数据 进行字节到长字的符号扩展,计算结果存放在寄存器edx中。下面是一 些允许的操作数扩展后缀: *bl: 字节-> 长字 *bw: 字节-> 字 *wl: 字-> 长字 还有一些其他的类型转换指令的对应关系:
*Intel *AT&T ⑴ cbw cbtw 符号扩展:al-> ax ⑵ cwde cwtl 符号扩展:ax-> eax ⑶ cwd cwtd 符号扩展:ax-> dx:ax ⑷ cdq cltd 符号扩展:eax-> edx:eax
还有一个不同名的助记符就是远程跳转/调用指令。在Intel格式中, 远程跳转/调用指令的助记符为“call/jmp far”,而在AT&T的汇编 语言中,相应的指令为“lcall”和“ljmp”。
②AT&T中寄存器的命名 在AT&T汇编语言中,寄存器操作数总是以 '% '作为前缀。80386芯片的 寄存器包括: ⑴8个32位寄存器: '%eax ', '%ebx ', '%ecx ', '%edx ', '%edi ', '%esi ', '%ebp ', '%esp ' ⑵8个16位寄存器: '%ax ', '%bx ', '%cx ', '%dx ', '%si ', '%di ', '%bp ', '%sp ' ⑶8个8位寄存器: '%ah ', '%al ', '%bh ', '%bl ', '%ch ', '%cl ', '%dh ', '%dl ' ⑷6个段寄存器: '%cs ', '%ds ', '%es ', '%ss ', '%fs ', '%gs ' ⑸3个控制寄存器: '%cr0 ', '%cr1 ', '%cr2 ' ⑹6个调试寄存器: '%db0 ', '%db1 ', '%db2 ', '%db3 ', '%db6 ', '%db7 ' ⑺2个测试寄存器: '%tr6 ', '%tr7 ' ⑻8个浮点寄存器栈: '%st(0) ', '%st(1) ', '%st(2) ', '%st(3) ', '%st(4) ', '%st(5) ', '%st(6) ', '%st(7) '
*注:我对这些寄存器并不是都了解,这些资料只是摘自as.info文档。 如果真的需要寄存器命名的资料,我想可以参考一下相应GNU工具的机 器描述方面的源文件。
③AT&T中的操作码前缀 ⑴段超越前缀 'cs ', 'ds ', 'es ', 'ss ', 'fs ', 'gs ':当汇编程序中对内 存操作数进行SECTION:MEMORY-OPERAND引用时,自动加上相应的段超 越前缀。 ⑵操作数/地址尺寸前缀 'data16 ', 'addr16 ':这些前缀将32位的操作 数/地址转化为16位的操作数/地址。 ⑶总线锁定前缀 'lock ':总线锁定操作。 'lock '前缀在Linux核心代码 中使用很多,特别是SMP代码中。 ⑷协处理器等待前缀 'wait ':等待协处理器完成当前操作。 ⑸指令重复前缀 'rep ', 'repe ', 'repne ':在串操作中重复指令的执行。
④AT&T中的内存操作数 在Intel的汇编语言中,内存操作数引用的格式如下:
SECTION:[BASE + INDEX*SCALE + DISP] 而在AT&T的汇编语言中,内存操作数的应用格式则是这样的:
SECTION:DISP(BASE,INDEX,SCALE)
下面是一些内存操作数的例子:
*AT&T *Intel ⑴ -4(%ebp) [ebp-4] ⑵ foo(,%eax,4) [foo+eax*4] ⑶ foo(,1) [foo] ⑷ %gs:foo gs:foo
还有,绝对跳转/调用指令中的内存操作数必须以 '* '最为前缀,否则 as总是假设这是一个相对跳转/调用指令。
⑤AT&T中的跳转指令 as汇编程序自动对跳转指令进行优化,总是使用尽可能小的跳转偏移 量。如果8比特的偏移量无法满足要求的话,as会使用一个32位的偏 移量,as汇编程序暂时还不支持16位的跳转偏移量,所以对跳转指令 使用 'addr16 '前缀是无效的。
还有一些跳转指令只支持8位的跳转偏移量,这些指令包括: 'jcxz ', 'jecxz ', 'loop ', 'loopz ', 'loope ', 'loopnz '和 'loopne '。所以, 在as的汇编源程序中使用这些指令可能会出错。(幸运的是,gcc并 不使用这些指令)
对AT&T汇编语言语法的简单介绍差不多了,其中有些特性是as特有的。 在Linux核心代码中,并不涉及到所有上面这些提到的语法规则,其 中有两点规则特别重要:第一,as中对寄存器引用时使用前缀 '% ';第 二,AT&T汇编语言中源操作数和目标操作数的位置与我们熟悉的Intel 的语法正好相反。
四.gcc的内嵌汇编语言语句asm 利用gcc的asm语句,你可以在C语言代码中直接嵌入汇编语言指令, 同时还可以使用C语言的表达式指定汇编指令所用到的操作数。这一 特性提供了很大的方便。
要使用这一特性,首先要写一个汇编指令的模板(这种模板有点类似 于机器描述文件中的指令模板),然后要为每一个操作数指定一个限 定字符串。例如: extern __inline__ void change_bit(int nr,volatile void *addr) {
__asm__ __volatile__( LOCK_PREFIX
"btcl %1,%0 "
: "=m " (ADDR)
: "ir " (nr)); } 上面的函数中:
LOCK_PREFIX:这是一个宏,如果定义了__SMP__,扩展为 "lock; ", 用于指定总线锁定前缀,否则扩展为 " "。
ADDR:这也是一个宏,定义为(*(volatile struct __dummy *) addr)
"btcl %1,%0 ":这就是嵌入的汇编语言指令,btcl为指令操作码,%1, %0是这条指令两个操作数的占位符。后面的两个限定字符串就用于描 述这两个操作数。
: "=m " (ADDR):第一个冒号后的限定字符串用于描述指令中的“输 出”操作数。刮号中的ADDR将操作数与C语言的变量联系起来。这个 限定字符串表示指令中的“%0”就是addr指针指向的内存操作数。这 是一个“输出”类型的内存操作数。
: "ir " (nr):第二个冒号后的限定字符串用于描述指令中的“输入” 操作数。这条限定字符串表示指令中的“%1”就是变量nr,这个的操 作数可以是一个立即操作数或者是一个寄存器操作数。
*注:限定字符串与操作数占位符之间的对应关系是这样的:在所有 限定字符串中(包括第一个冒号后的以及第二个冒号后的所有限定字 符串),最先出现的字符串用于描述操作数“%0”,第二个出现的字 符串描述操作数“%1”,以此类推。
①汇编指令模板 asm语句中的汇编指令模板
|