[转贴]AT&T ASM Syntax,帮大家了解linux代码中的AT&T汇编

在开始之前先介绍一下寄存器(不是针对AT&T汇编的)

1.通用寄存器(用途是计算)

EAX
32-bit宽

通用寄存器。相对其他寄存器,在进行运算方面比较常用。在保护模式中,也可以作为内存偏移指针(此时,DS作为段 寄存器或选择器)

EBX
32-bit宽

通用寄存器。通常作为内存偏移指针使用(相对于EAX、ECX、EDX),DS是默认的段寄存器或选择器。在保护模式中,同样可以起这个作用。

ECX
32-bit宽

通用寄存器。通常用于特定指令的计数。在保护模式中,也可以作为内存偏移指针(此时,DS作为 寄存器或段选择器)。

EDX
32-bit宽

通用寄存器。在某些运算中作为EAX的溢出寄存器(例如乘、除)。在保护模式中,也可以作为内存偏移指针(此时,DS作为段 寄存器或选择器)。

2. 内存指针的特殊寄存器

ESI
32-bit宽

通常在内存操作指令中作为“源地址指针”使用。当然,ESI可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器。

EDI
32-bit宽

通常在内存操作指令中作为“目的地址指针”使用。当然,EDI也可以被装入任意的数值,但通常没有人把它当作通用寄存器来用。DS是默认段寄存器或选择器。

EBP
32-bit宽

这也是一个作为指针的寄存器。通常,它被高级语言编译器用以建造‘堆栈帧’来保存函数或过程的局部变量,不过,还是那句话,你可以在其中保存你希望的任何数据。SS是它的默认段寄存器或选择器。
3、 段寄存器和选择器  
CS代码段,或代码选择器。同IP寄存器(稍后介绍)一同指向当前正在执行的那个地址。处理器执行时从这个寄存器指向的段(实模式)或内存(保护模式)中获取指令。除了跳转或其他分支指令之外,你无法修改这个寄存器的内容。
DS数 据段,或数据选择器。这个寄存器的低16 bit连同ESI一同指向的指令将要处理的内存。同时,所有的内存操作指令 默认情况下都用它指定操作段(实模式)或内存(作为选择器,在保护模式。这个寄存器可以被装入任意数值,然而在这么做的时候需要小心一些。方法是,首先把 数据送给AX,然后再把它从AX传送给DS(当然,也可以通过堆栈来做).
ES附加段,或附加选择器。这个寄存器的低16 bit连同EDI一同指向的指令将要处理的内存。同样的,这个寄存器可以被装入任意数值,方法和DS类似。
FSF段或F选择器(推测F可能是Free?)。可以用这个寄存器作为默认段寄存器或选择器的一个替代品。它可以被装入任何数值,方法和DS类似。
GSG段或G选择器(G的意义和F一样,没有在Intel的文档中解释)。它和FS几乎完全一样。
SS堆 栈段或堆栈选择器。这个寄存器的低16bit连同ESP一同指向下一次堆栈操作(push和pop)所要使用的堆栈地址。这个寄存器也可以被装入任意数 值,你可以通过入栈和出栈操作来给他赋值,不过由于堆栈对于很多操作有很重要的意义,因此,不正确的修改有可能造成对堆栈的破坏。
4、 特殊寄存器(指向到特定段或内存的偏移量):
EIP这个寄存器非常的重要。这是一个32位宽的寄存器 ,同CS一同指向即将执行的那条指令的地址。不能够直接修改这个寄存器的值,修改它的唯一方法是跳转或分支指令。(CS是默认的段或选择器)
ESP这个32位寄存器指向堆栈中即将被操作的那个地址。尽管可以修改它的值,然而并不提倡这样做,因为如果你不是非常明白自己在做什么,那么你可能造成堆栈的破坏。对于绝大多数情况而言,这对程序是致命的。(SS是默认的段或选择器)

IP: Instruction Pointer, 指令指针       SP: Stack Pointer, 堆栈指针

 

 

---------------------------------------------------------------
下文是引用"Darwin yuan"在
《Developing Your Own OS On IBM PC》一书中的章节(Misa注)
--------------------------------------------------------------------------
0.4.1 Overview  
开发一个OS,尽管绝大部分代码只需要用C/C++等高级语言就可以了,但至少和硬件相关部分的代码需要使用汇编语言,另外,由于启动部分的代码有大小限制,使用精练的汇编可以缩小目标代码的Size。另外,对于某些需要被经常调用的代码,使用汇编来写可以提高性能。所以我们必须了解汇编语言,即使你有可能并不喜欢它。

如果你是计算机专业的话,在大学里你应该学习过Intel格式的8086/80386汇编,这里就不再讨论。如果我们选择的OS开发工具是GCC以及GAS的话,就必须了解AT&T汇编语言语法,因为GCC/GAS只支持这种汇编语法。

本书不会去讨论8086/80386的汇编编程,这类的书籍很多,你可以参考它们。这里只会讨论AT&T的汇编语法,以及GCC的内嵌汇编语法。

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

0.4.2 Syntax
1.寄存器引用
引用寄存器要在寄存器号前加百分号%,如“movl %eax, %ebx”。
80386有如下寄存器:
8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
8个16-bit寄存器,它们事实上是上面8个32-bit寄存器的低16位:%ax,%bx,%cx,%dx,%di,%si,%bp,%sp;
8个8-bit寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它们事实上是寄存器%ax,%bx,%cx,%dx的高8位和低8位;
6个段寄存器:%cs(code),%ds(data),%ss(stack), %es,%fs,%gs;
3个控制寄存器:%cr0,%cr2,%cr3;
6个debug寄存器:%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)。
2. 操作数顺序
操作数排列是从源(左)到目的(右),如“movl %eax(源), %ebx(目的)”
3. 立即数
使用立即数,要在数前面加符号$, 如“movl $0x04, %ebx”
或者:
para = 0x04
movl $para, %ebx
指令执行的结果是将立即数04h装入寄存器ebx。
4. 符号常数
符号常数直接引用 如
value: .long 0x12a3f2de
movl value , %ebx
指令执行的结果是将常数0x12a3f2de装入寄存器ebx。
引用符号地址在符号前加符号$, 如“movl $value, % ebx”则是将符号value的地址装入寄存器ebx。
5. 操作数的长度
操作数的长度用加在指令后的符号表示b(byte, 8-bit), w(word, 16-bits), l(long, 32-bits),如“movb %al, %bl”,“movw %ax, %bx”,“movl %eax, %ebx ”。
如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov %ax, %bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于“movw %ax, %bx”。同样道理,指令“mov $4, %ebx”等同于指令“movl $4, %ebx”,“push %al”等同于“pushb %al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push $4”。
6. 符号扩展和零扩展指令
绝大多数面向80386的AT&T汇编指令与Intel格式的汇编指令都是相同的,符号扩展指令和零扩展指令则是仅有的不同格式指令。
符号扩展指令和零扩展指令需要指定源操作数长度和目的操作数长度,即使在某些指令中这些操作数是隐含的。
在AT&T语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz"(对应Intel语法的movsx和movzx),后面跟上源操作数长度和目的操作数长度。movsbl意味着movs (from)byte (to)long;movbw意味着movs (from)byte (to)word;movswl意味着movs (from)word (to)long。对于movz指令也一样。比如指令“movsbl %al, %edx”意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。
其它的Intel格式的符号扩展指令还有:
cbw -- sign-extend byte in %al to word in %ax;
cwde -- sign-extend word in %ax to long in %eax;
cwd -- sign-extend word in %ax to long in %dx:%ax;
cdq -- sign-extend dword in %eax to quad in %edx:%eax;
对应的AT&T语法的指令为cbtw,cwtl,cwtd,cltd。
7. 调用和跳转指令
段内调用和跳转指令为"call","ret"和"jmp",段间调用和跳转指令为"lcall","lret"和"ljmp"。
段间调用和跳转指令的格式为“lcall/ljmp $SECTION, $OFFSET”,而段间返回指令则为“lret $STACK-ADJUST”。
8. 前缀
操作码前缀被用在下列的情况:
字符串重复操作指令(rep,repne);
指定被操作的段(cs,ds,ss,es,fs,gs);
进行总线加锁(lock);
指定地址和操作的大小(data16,addr16);
在AT&T汇编语法中,操作码前缀通常被单独放在一行,后面不跟任何操作数。例如,对于重复scas指令,其写法为:
             repne
             scas
上述操作码前缀的意义和用法如下:
指定被操作的段前缀为cs,ds,ss,es,fs,和gs。在AT&T语法中,只需要按照section:memory-operand的格式就指定了相应的段前缀。比如:lcall %cs:realmode_swtch
操作数/地址大小前缀是“data16”和"addr16",它们被用来在32-bit操作数/地址代码中指定16-bit的操作数/地址。
总线加锁前缀“lock”,它是为了在多处理器环境中,保证在当前指令执行期间禁止一切中断。这个前缀仅仅对ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD,XCHG指令有效,如果将Lock前缀用在其它指令之前,将会引起异常。
字符串重复操作前缀"rep","repe","repne"用来让字符串操作重复“%ecx”次。
9. 内存引用
Intel语法的间接内存引用的格式为:
section:[base+index*scale+displacement]
而在AT&T语法中对应的形式为:
section:displacement(base,index,scale)
其中,base和index是任意的32-bit base和index寄存器。scale可以取值1,2,4,8。如果不指定scale值,则默认值为1。section可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。如果你在指令中指定了默认的段前缀,则编译器在目标代码中不会产生此段前缀代码。
下面是一些例子:
-4(%ebp):base=%ebp,displacement=-4,section没有指定,由于base=%ebp,所以默认的section=%ss,index,scale没有指定,则index为0。
foo(,%eax,4):index=%eax,scale=4,displacement=foo。其它域没有指定。这里默认的section=%ds。
foo(,1):这个表达式引用的是指针foo指向的地址所存放的值。注意这个表达式中没有base和index,并且只有一个逗号,这是一种异常语法,但却合法。
%gs:foo:这个表达式引用的是放置于%gs段里变量foo的值。
如果call和jump操作在操作数前指定前缀“*”,则表示是一个绝对地址调用/跳转,也就是说jmp/call指令指定的是一个绝对地址。如果没有指定"*",则操作数是一个相对地址。
任何指令如果其操作数是一个内存操作,则指令必须指定它的操作尺寸(byte,word,long),也就是说必须带有指令后缀(b,w,l)。

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


0.4.3 GCC Inline ASM
GCC支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作GCC Inline ASM——GCC内联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法表达的指令直接潜入C/C++代码中,另外也允许我们直接写C/C++代码中使用汇编编写简洁高效的代码。
1.基本内联汇编
GCC中基本的内联汇编非常易懂,我们先来看两个简单的例子:
__asm__("movl %esp,%eax");  // 看起来很熟悉吧!
或者是
__asm__("
   movl $1,%eax  // SYS_exit
   xor %ebx,%ebx
   int $0x80
");

__asm__(
   "movl $1,%eax/r/t" /
   "xor %ebx,%ebx/r/t" /
   "int $0x80" /
   );
基本内联汇编的格式是
__asm__ __volatile__("Instruction List");

1、__asm__
__asm__是GCC关键字asm的宏定义:
#define __asm__ asm
__asm__或asm用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。
2、Instruction List
Instruction List是汇编指令序列。它可以是空的,比如:__asm__ __volatile__(""); 或__asm__ ("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。但并非所有Instruction List为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory"); 就非常有意义,它向GCC声明:“我对内存作了改动”,GCC在编译的时候,会将此因素考虑进去。
我们看一看下面这个例子:
$ cat example1.c
int main(int __argc, char* __argv[])  
{                                    
        int* __p = (int*)__argc;      
                                      
        (*__p) = 9999;               
                                      
        //__asm__("":::"memory");      
                                      
        if((*__p) == 9999)            
                return 5;            
                                      
        return (*__p);               
}
在这段代码中,那条内联汇编是被注释掉的。在这条内联汇编之前,内存指针__p所指向的内存被赋值为9999,随即在内联汇编之后,一条if语句判断__p所指向的内存与9999是否相等。很明显,它们是相等的。GCC在优化编译的时候能够很聪明的发现这一点。我们使用下面的命令行对其进行编译:
$ gcc -O -S example1.c
选项-O表示优化编译,我们还可以指定优化等级,比如-O2表示优化等级为2;选项-S表示将C/C++源文件编译为汇编文件,文件名和C/C++文件一样,只不过扩展名由.c变为.s。
我们来查看一下被放在example1.s中的编译结果,我们这里仅仅列出了使用gcc 2.96在redhat 7.3上编译后的相关函数部分汇编代码。为了保持清晰性,无关的其它代码未被列出。
$ cat example1.s
main:                        
        pushl   %ebp         
        movl    %esp, %ebp   
        movl    8(%ebp), %eax     # int* __p = (int*)__argc
        movl    $9999, (%eax)      # (*__p) = 9999
        movl    $5, %eax              # return 5
        popl    %ebp         
        ret
参照一下C源码和编译出的汇编代码,我们会发现汇编代码中,没有if语句相关的代码,而是在赋值语句(*__p)=9999后直接return 5;这是因为GCC认为在(*__p)被赋值之后,在if语句之前没有任何改变(*__p)内容的操作,所以那条if语句的判断条件(*__p) == 9999肯定是为true的,所以GCC就不再生成相关代码,而是直接根据为true的条件生成return 5的汇编代码(GCC使用eax作为保存返回值的寄存器)。
我们现在将example1.c中内联汇编的注释去掉,重新编译,然后看一下相关的编译结果。
$ gcc -O -S example1.c
$ cat example1.s
main:                          
        pushl   %ebp           
        movl    %esp, %ebp     
        movl    8(%ebp), %eax   # int* __p = (int*)__argc
        movl    $9999, (%eax)    # (*__p) = 9999
#APP                           
                                               # __asm__("":::"memory")
#NO_APP
        cmpl    $9999, (%eax)    # (*__p) == 9999 ?
        jne     .L3                        # false   
        movl    $5, %eax            # true, return 5
        jmp     .L2            
        .p2align 2            
.L3:                           
        movl    (%eax), %eax   
.L2:                           
        popl    %ebp           
        ret
由于内联汇编语句__asm__("":::"memory")向GCC声明,在此内联汇编语句出现的位置内存内容可能了改变,所以GCC在编译时就不能像刚才那样处理。这次,GCC老老实实的将if语句生成了汇编代码。
可能有人会质疑:为什么要使用__asm__("":::"memory")向GCC声明内存发生了变化?明明“Instruction List”是空的,没有任何对内存的操作,这样做只会增加GCC生成汇编代码的数量。
确实,那条内联汇编语句没有对内存作任何操作,事实上它确实什么都没有做。但影响内存内容的不仅仅是你当前正在运行的程序。比如,如果你现在正在操作的内存是一块内存映射,映射的内容是外围I/O设备寄存器。那么操作这块内存的就不仅仅是当前的程序,I/O设备也会去操作这块内存。既然两者都会去操作同一块内存,那么任何一方在任何时候都不能对这块内存的内容想当然。所以当你使用高级语言C/C++写这类程序的时候,你必须让编译器也能够明白这一点,毕竟高级语言最终要被编译为汇编代码。
你可能已经注意到了,这次输出的汇编结果中,有两个符号:#APP和#NO_APP,GCC将内联汇编语句中"Instruction List"所列出的指令放在#APP和#NO_APP之间,由于__asm__("":::"memory")中“Instruction List”为空,所以#APP和#NO_APP中间也没有任何内容。但我们以后的例子会更加清楚的表现这一点。
关于为什么内联汇编__asm__("":::"memory")是一条声明内存改变的语句,我们后面会详细讨论。
刚才我们花了大量的内容来讨论"Instruction List"为空是的情况,但在实际的编程中,"Instruction List"绝大多数情况下都不是空的。它可以有1条或任意多条汇编指令。
当在"Instruction List"中有多条指令的时候,你可以在一对引号中列出全部指令,也可以将一条或几条指令放在一对引号中,所有指令放在多对引号中。如果是前者,你可以将每一条指令放在一行,如果要将多条指令放在一行,则必须用分号(;)或换行符(/n,大多数情况下/n后还要跟一个/t,其中/n是为了换行,/t是为了空出一个tab宽度的空格)将它们分开。比如:
__asm__("movl %eax, %ebx   
                   sti               
                   popl %edi         
                   subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti         
                   popl %edi; subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti/n/t popl %edi
                   subl %ecx, %ebx");
都是合法的写法。如果你将指令放在多对引号中,则除了最后一对引号之外,前面的所有引号里的最后一条指令之后都要有一个分号(;)或(/n)或(/n/t)。比如:
__asm__("movl %eax, %ebx   
                    sti/n"               
                   "popl %edi;"         
                   "subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti/n/t"         
                 "popl %edi; subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti/n/t popl %edi/n"
                 "subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti/n/t popl %edi;" "subl %ecx, %ebx");
都是合法的。
上述原则可以归结为:
任意两个指令间要么被分号(;)分开,要么被放在两行;
放在两行的方法既可以从通过/n的方法来实现,也可以真正的放在两行;
可以使用1对或多对引号,每1对引号里可以放任一多条指令,所有的指令都要被放到引号中。
在基本内联汇编中,“Instruction List”的书写的格式和你直接在汇编文件中写非内联汇编没有什么不同,你可以在其中定义Label,定义对齐(.align n ),定义段(.section name )。例如:
__asm__(".align 2/n/t"         
                 "movl %eax, %ebx/n/t"  
                 "test %ebx, %ecx/n/t"  
                 "jne error/n/t"        
                 "sti/n/t"              
                 "error: popl %edi/n/t"
                 "subl %ecx, %ebx");
上面例子的格式是Linux内联代码常用的格式,非常整齐。也建议大家都使用这种格式来写内联汇编代码。

3、__volatile__
__volatile__是GCC关键字volatile的宏定义:
#define __volatile__ volatile
__volatile__或volatile是可选的,你可以用它也可以不用它。如果你用了它,则是向GCC声明“不要动我所写的Instruction List,我需要原封不动的保留每一条指令”,否则当你使用了优化选项(-O)进行编译时,GCC将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。
那么GCC判断的原则是什么?我不知道(如果有哪位朋友清楚的话,请告诉我)。我试验了一下,发现一条内联汇编语句如果是基本内联汇编的话(即只有“Instruction List”,没有Input/Output/Clobber的内联汇编,我们后面将会讨论这一点),无论你是否使用__volatile__来修饰,GCC 2.96在优化编译时,都会原封不动的保留内联汇编中的“Instruction List”。但或许我的试验的例子并不充分,所以这一点并不能够得到保证。
为了保险起见,如果你不想让GCC的优化影响你的内联汇编代码,你最好在前面都加上__volatile__,而不要依赖于编译器的原则,因为即使你非常了解当前编译器的优化原则,你也无法保证这种原则将来不会发生变化。而__volatile__的含义却是恒定的。
2、带有C/C++表达式的内联汇编
GCC允许你通过C/C++表达式指定内联汇编中"Instrcuction List"中指令的输入和输出,你甚至可以不关心到底使用哪个寄存器被使用,完全靠GCC来安排和指定。这一点可以让程序员避免去考虑有限的寄存器的使用,也可以提高目标代码的效率。
我们先来看几个例子:
__asm__ (" " : : : "memory" );  // 前面提到的
__asm__ ("mov %%eax, %%ebx" : "=b"(rv) : "a"(foo) : "eax", "ebx");
__asm__ __volatile__("lidt %0": "=m" (idt_descr));
__asm__("subl %2,%0/n/t"
                "sbbl %3,%1"
                : "=a" (endlow), "=d" (endhigh)
                : "g" (startlow), "g" (starthigh), "0" (endlow), "1" (endhigh));
怎么样,有点印象了吧,是不是也有点晕?没关系,下面讨论完之后你就不会再晕了。(当然,也有可能更晕^_^)。讨论开始——
带有C/C++表达式的内联汇编格式为:
__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
从中我们可以看出它和基本内联汇编的不同之处在于:它多了3个部分(Input,Output,Clobber/Modify)。在括号中的4个部分通过冒号( 分开。
这4个部分都不是必须的,任何一个部分都可以为空,其规则为:
如果Clobber/Modify为空,则其前面的冒号( 必须省略。比如__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的写法;而__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) )则是正确的。
如果Instruction List为空,则Input,Output,Clobber/Modify可以不为空,也可以为空。比如__asm__ ( " " : : : "memory" );和__asm__(" " : : );都是合法的写法。
如果Output,Input,Clobber/Modify都为空,Output,Input之前的冒号( 既可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内联汇编,否则,仍然是一个带有C/C++表达式的内联汇编,此时"Instruction List"中的寄存器写法要遵守相关规定,比如寄存器前必须使用两个百分号(%%),而不是像基本汇编格式一样在寄存器前只使用一个百分号(%)。比如__asm__( " mov %%eax, %%ebx" : : );__asm__( " mov %%eax, %%ebx" : )和__asm__( " mov %eax, %ebx" )都是正确的写法,而__asm__( " mov %eax, %ebx" : : );__asm__( " mov %eax, %ebx" : )和__asm__( " mov %%eax, %%ebx" )都是错误的写法。
如果Input,Clobber/Modify为空,但Output不为空,Input前的冒号(:)既可以省略,也可以不省略。比如__asm__( " mov %%eax, %%ebx" : "=b"(foo) : );__asm__( " mov %%eax, %%ebx" : "=b"(foo) )都是正确的。
如果后面的部分不为空,而前面的部分为空,则前面的冒号(:)都必须保留,否则无法说明不为空的部分究竟是第几部分。比如, Clobber/Modify,Output为空,而Input不为空,则Clobber/Modify前的冒号必须省略(前面的规则),而Output前的冒号必须为保留。如果Clobber/Modify不为空,而Input和Output都为空,则Input和Output前的冒号都必须保留。比如__asm__( " mov %%eax, %%ebx" : : "a"(foo) )和__asm__( " mov %%eax, %%ebx" : : : "ebx" )。
从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有C/C++表达式格式的,其规则在于在"Instruction List"后是否有冒号(:)的存在,如果没有则是基本格式的,否则,则是带有C/C++表达式格式的。
两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个百分号(%),这一点和非内联汇编相同;而带有C/C++表达式格式则要求寄存器前必须使用两个百分号(%%),其原因我们会在后面讨论。
1. Output
Output用来指定当前内联汇编语句的输出。我们看一看这个例子:
__asm__("movl %%cr0, %0": "=a" (cr0));
这个内联汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,指定了一个输出操作。我们可以很清楚得看到这个输出操作由两部分组成:括号括住的部分(cr0)和引号引住的部分"=a"。这两部分都是每一个输出操作必不可少的。括号括住的部分是一个C/C++表达式,用来保存内联汇编的一个输出值,其操作就等于C/C++的相等赋值cr0 = output_value,因此,括号中的输出表达式只能是C/C++的左值表达式,也就是说它只能是一个可以合法的放在C/C++赋值操作中等号(=)左边的表达式。那么右值output_value从何而来呢?
答案是引号中的内容,被称作“操作约束”(Operation Constraint),在这个例子中操作约束为"=a",它包含两个约束:等号(=)和字母a,其中等号(=)说明括号中左值表达式cr0是一个Write-Only的,只能够被作为当前内联汇编的输入,而不能作为输入。而字母a是寄存器EAX / AX / AL的简写,说明cr0的值要从eax寄存器中获取,也就是说cr0 = eax,最终这一点被转化成汇编指令就是movl %eax, address_of_cr0。现在你应该清楚了吧,操作约束中会给出:到底从哪个寄存器传递值给cr0。
另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并非如此。因为等号(=)约束说明当前的表达式是一个Write-Only的,但另外还有一个符号——加号(+)用来说明当前表达式是一个Read-Write的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是Read-Only的。因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+)都表示可写,只不过加号(+)同时也表示是可读的。所以对于一个输出操作来说,其操作约束只需要有等号(=)或加号(+)中的任意一个就可以了。
二者的区别是:等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(=)约束还是加号(+)约束所约束的操作表达式都只能放在Output域中,而不能被用在Input域中。
另外,有些文档声明:尽管GCC文档中提供了加号(+)约束,但在实际的编译中通不过;我不知道老版本会怎么样,我在GCC 2.96中对加号(+)约束的使用非常正常。
我们通过一个例子看一下,在一个输出操作中使用等号(=)约束和加号(+)约束的不同。
$ cat example2.c
int main(int __argc, char* __argv[])                           
{                                                              
        int cr0 = 5;                                             
                                                               
        __asm__ __volatile__("movl %%cr0, %0":"=a" (cr0));     
                                                               
        return 0;                                             
}
$ gcc -S example2.c
$ cat example2.s
main:                        
        pushl   %ebp         
        movl    %esp, %ebp   
        subl    $4, %esp      
        movl    $5, -4(%ebp)     # cr0 = 5
#APP                          
        movl %cr0, %eax      
#NO_APP                       
        movl    %eax, %eax   
        movl    %eax, -4(%ebp)   # cr0 = %eax
        movl    $0, %eax      
        leave                 
        ret                  

这个例子是使用等号(=)约束的情况,变量cr0被放在内存-4(%ebp)的位置,所以指令mov %eax, -4(%ebp)即表示将%eax的内容输出到变量cr0中。
下面是使用加号(+)约束的情况:
$ cat example3.c
int main(int __argc, char* __argv[])                           
{                                                              
        int cr0 = 5;                                             
                                                               
        __asm__ __volatile__("movl %%cr0, %0" : "+a" (cr0));     
                                                               
        return 0;                                             
}
$ gcc -S example3.c
$ cat example3.s
main:                           
        pushl   %ebp            
        movl    %esp, %ebp      
        subl    $4, %esp         
        movl    $5, -4(%ebp)        # cr0 = 5
        movl    -4(%ebp), %eax    # input ( %eax = cr0 )
#APP                             
        movl    %cr0, %eax
#NO_APP
        movl    %eax, -4(%ebp)    # output (cr0 = %eax )
        movl    $0, %eax
        leave
        ret

从编译的结果可以看出,当使用加号(+)约束的时候,cr0不仅作为输出,还作为输入,所使用寄存器都是寄存器约束(字母a,表示使用eax寄存器)指定的。关于寄存器约束我们后面讨论。
在Output域中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。例如:
__asm__(                           
        "movl   %%eax, %0 /n/t"              
        "pushl  %%ebx /n/t"                 
        "popl    %1 /n/t"                     
        "movl   %1, %2"                     
        : "+a"(cr0), "=b"(cr1), "=c"(cr2));

2、Input
Input域的内容用来指定当前内联汇编语句的输入。我们看一看这个例子:
__asm__("movl %0, %%db7" : : "a" (cpu->db7));
例中Input域的内容为一个表达式"a"[cpu->db7),被称作“输入表达式”,用来表示一个对当前内联汇编的输入。
像输出表达式一样,一个输入表达式也分为两部分:带括号的部分(cpu->db7)和带引号的部分"a"。这两部分对于一个内联汇编输入表达式来说也是必不可少的。
括号中的表达式cpu->db7是一个C/C++语言的表达式,它不必是一个左值表达式,也就是说它不仅可以是放在C/C++赋值操作左边的表达式,还可以是放在C/C++赋值操作右边的表达式。所以它可以是一个变量,一个数字,还可以是一个复杂的表达式(比如a+b/c*d)。比如上例可以改为:__asm__("movl %0, %%db7" : : "a" (foo)),__asm__("movl %0, %%db7" : : "a" (0x1000))或__asm__("movl %0, %%db7" : : "a" (va*vb/vc))。
引号号中的部分是约束部分,和输出表达式约束不同的是,它不允许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的Read-Only的。约束中必须指定一个寄存器约束,例中的字母a表示当前输入变量cpu->db7要通过寄存器eax输入到当前内联汇编中。
我们看一个例子:
$ cat example4.c
int main(int __argc, char* __argv[])                        
{                                                            
        int cr0  = 5;                                       
                                                            
        __asm__ __volatile__("movl %0, %%cr0"::"a" (cr0));   
                                                            
        return 0;                                            
}
$ gcc -S example4.c
$ cat example4.s
main:                              
        pushl   %ebp               
        movl    %esp, %ebp         
        subl    $4, %esp           
        movl    $5, -4(%ebp)          # cr0 = 5      
        movl    -4(%ebp), %eax     # %eax = cr0
#APP                              
        movl    %eax, %cr0            
#NO_APP                           
        movl    $0, %eax           
        leave                     
        ret                        

我们从编译出的汇编代码可以看到,在"Instruction List"之前,GCC按照我们的输入约束"a",将变量cr0的内容装入了eax寄存器。
3. Operation Constraint
每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint,我们这里来讨论在80386平台上所可能使用的操作约束。
1、寄存器约束
当你当前的输入或输入需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可以直接指定一个寄存器的名字,比如:
__asm__ __volatile__("movl %0, %%cr0"::"eax" (cr0));
也可以指定一个缩写,比如:
__asm__ __volatile__("movl %0, %%cr0"::"a" (cr0));
如果你指定一个缩写,比如字母a,则GCC将会根据当前操作表达式中C/C++表达式的宽度决定使用%eax,还是%ax或%al。比如:
unsigned short __shrt;
__asm__ ("mov %0,%%bx" : : "a"(__shrt));
由于变量__shrt是16-bit short类型,则编译出来的汇编代码中,则会让此变量使用%ex寄存器。编译结果为:
       movw    -2(%ebp), %ax  # %ax = __shrt
#APP
        movl    %ax, %bx
#NO_APP
无论是Input,还是Output操作表达式约束,都可以使用寄存器约束。
下表中列出了常用的寄存器约束的缩写。
约束 Input/Output 意义
r I,O 表示使用一个通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中选取一个GCC认为合适的。
q I,O 表示使用一个通用寄存器,和r的意义相同。
a I,O 表示使用%eax / %ax / %al
b I,O 表示使用%ebx / %bx / %bl
c I,O 表示使用%ecx / %cx / %cl
d I,O 表示使用%edx / %dx / %dl
D I,O 表示使用%edi / %di
S I,O 表示使用%esi / %si
f I,O 表示使用浮点寄存器
t I,O 表示使用第一个浮点寄存器
u I,O 表示使用第二个浮点寄存器

2、内存约束
如果一个Input/Output操作表达式的C/C++表达式表现为一个内存地址,不想借助于任何寄存器,则可以使用内存约束。比如:
__asm__ ("lidt %0" : "=m"(__idt_addr)); 或 __asm__ ("lidt %0" : :"m"(__idt_addr));
我们看一下它们分别被放在一个C源文件中,然后被GCC编译后的结果:
$ cat example5.c
// 本例中,变量sh被作为一个内存输入
int main(int __argc, char* __argv[])               
{                                                  
         char* sh = (char*)&__argc;               
                                                   
        __asm__ __volatile__("lidt %0" : : "m" (sh));
                                                   
        return 0;                                 
}
$ gcc -S example5.c
$ cat example5.s
main:                        
        pushl   %ebp         
        movl    %esp, %ebp   
        subl    $4, %esp      
        leal     8(%ebp), %eax
        movl   %eax, -4(%ebp)  # sh = (char*) &__argc
#APP                          
        lidt      -4(%ebp)         
#NO_APP                       
        movl    $0, %eax      
        leave                 
        ret                  

$ cat example6.c
// 本例中,变量sh被作为一个内存输出
int main(int __argc, char* __argv[])               
{                                                  
         char* sh = (char*)&__argc;               
                                                   
        __asm__ __volatile__("lidt %0" : "=m" (sh));
                                                   
        return 0;                                 
}         
$ gcc -S example6.c
$ cat example6.s
main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $4, %esp
        leal     8(%ebp), %eax
        movl    %eax, -4(%ebp) # sh = (char*) &__argc
#APP
        lidt      -4(%ebp)
#NO_APP
        movl    $0, %eax
        leave
        ret
首先,你会注意到,在这两个例子中,变量sh没有借助任何寄存器,而是直接参与了指令lidt的操作。
其次,通过仔细观察,你会发现一个惊人的事实,两个例子编译出来的汇编代码是一样的!虽然,一个例子中变量sh作为输入,而另一个例子中变量sh作为输出。这是怎么回事?
原来,使用内存方式进行输入输出时,由于不借助寄存器,所以GCC不会按照你的声明对其作任何的输入输出处理。GCC只会直接拿来用,究竟对这个C/C++表达式而言是输入还是输出,完全依赖与你写在"Instruction List"中的指令对其操作的指令。
由于上例中,对其操作的指令为lidt,lidt指令的操作数是一个输入型的操作数,所以事实上对变量sh的操作是一个输入操作,即使你把它放在Output域也不会改变这一点。所以,对此例而言,完全符合语意的写法应该是将sh放在Input域,尽管放在Output域也会有正确的执行结果。
所以,对于内存约束类型的操作表达式而言,放在Input域还是放在Output域,对编译结果是没有任何影响的,因为本来我们将一个操作表达式放在Input域或放在Output域是希望GCC能为我们自动通过寄存器将表达式的值输入或输出。既然对于内存约束类型的操作表达式来说,GCC不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。
约束 Input/Output 意义
m I,O 表示使用系统所支持的任何一种内存方式,不需要借助寄存器

3、立即数约束
如果一个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器,则可以使用立即数约束。
由于立即数在C/C++中只能作为右值,所以对于使用立即数约束的表达式而言,只能放在Input域。
比如:__asm__ __volatile__("movl %0, %%eax" : : "i" (100) );
立即数约束很简单,也很容易理解,我们在这里就不再赘述。
约束 Input/Output 意义
i I 表示输入表达式是一个立即数(整数),不需要借助任何寄存器
F I 表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器

4、通用约束
约束 Input/Output 意义
g I,O 表示可以使用通用寄存器,内存,立即数等任何一种处理方式。
0,1,2,3,4,5,6,7,8,9 I 表示和第n个操作表达式使用相同的寄存器/内存。

通用约束g是一个非常灵活的约束,当程序员认为一个C/C++表达式在实际的操作中,究竟使用寄存器方式,还是使用内存方式或立即数方式并无所谓时,或者程序员想实现一个灵活的模板,让GCC可以根据不同的C/C++表达式生成不同的访问方式时,就可以使用通用约束g。比如:
#define JUST_MOV(foo)  __asm__ ("movl %0, %%eax" : : "g"(foo))
JUST_MOV(100)和JUST_MOV(var)则会让编译器产生不同的代码。
int main(int __argc, char* __argv[])   
{                                       
        JUST_MOV(100);                  
                                       
        return 0;                       
}                                       
编译后生成的代码为:
main:                       
        pushl   %ebp        
        movl    %esp, %ebp  
#APP                        
        movl $100, %eax     
#NO_APP                     
        movl    $0, %eax   
        popl    %ebp        
        ret
很明显这是立即数方式。而下一个例子:
int main(int __argc, char* __argv[])   
{                                       
        JUST_MOV(__argc);                  
                                       
        return 0;                       
}                                       
经编译后生成的代码为:
main:                     
        pushl   %ebp      
        movl    %esp, %ebp
#APP                       
        movl   8(%ebp), %eax
#NO_APP                    
        movl    $0, %eax   
        popl     %ebp      
        ret               
这个例子是使用内存方式。
一个带有C/C++表达式的内联汇编,其操作表达式被按照被列出的顺序编号,第一个是0,第2个是1,依次类推,GCC最多允许有10个操作表达式。比如:
__asm__ ("popl %0 /n/t"
                         "movl %1, %%esi /n/t"
                         "movl %2, %%edi /n/t"
                         : "=a"(__out)
                         : "r" (__in1), "r" (__in2));
此例中,__out所在的Output操作表达式被编号为0,"r"(__in1)被编号为1,"r"(__in2)被编号为2。
再如:
__asm__ ("movl %%eax, %%ebx" : : "a"(__in1), "b"(__in2));
此例中,"a"(__in1)被编号为0,"b"(__in2)被编号为1。
如果某个Input操作表达式使用数字0到9中的一个数字(假设为1)作为它的操作约束,则等于向GCC声明:“我要使用和编号为1的Output操作表达式相同的寄存器(如果Output操作表达式1使用的是寄存器),或相同的内存地址(如果Output操作表达式1使用的是内存)”。上面的描述包含两个限定:数字0到数字9作为操作约束只能用在Input操作表达式中,被指定的操作表达式(比如某个Input操作表达式使用数字1作为约束,那么被指定的就是编号为1的操作表达式)只能是Output操作表达式。
由于GCC规定最多只能有10个Input/Output操作表达式,所以事实上数字9作为操作约束永远也用不到,因为Output操作表达式排在Input操作表达式的前面,那么如果有一个Input操作表达式指定了数字9作为操作约束的话,那么说明Output操作表达式的数量已经至少为10个了,那么再加上这个Input操作表达式,则至少为11个了,以及超出GCC的限制。
5、Modifier Characters(修饰符)
等号(=)和加号(+)用于对Output操作表达式的修饰,一个Output操作表达式要么被等号(=)修饰,要么被加号(+)修饰,二者必居其一。使用等号(=)说明此Output操作表达式是Write-Only的,使用加号(+)说明此Output操作表达式是Read-Write的。它们必须被放在约束字符串的第一个字母。比如"a="(foo)是非法的,而"+g"(foo)则是合法的。
当使用加号(+)的时候,此Output表达式等价于使用等号(=)约束加上一个Input表达式。比如
__asm__ ("movl %0, %%eax; addl %%eax, %0" : "+b"(foo)) 等价于
__asm__ ("movl %1, %%eax; addl %%eax, %0" : "=b"(foo) : "b"(foo))
但如果使用后一种写法,"Instruction List"中的别名也要相应的改动。关于别名,我们后面会讨论。
像等号(=)和加号(+)修饰符一样,符号(&)也只能用于对Output操作表达式的修饰。当使用它进行修饰时,等于向GCC声明:"GCC不得为任何Input操作表达式分配与此Output操作表达式相同的寄存器"。其原因是&修饰符意味着被其修饰的Output操作表达式要在所有的Input操作表达式被输入前输出。我们看下面这个例子:
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中了。
下面就是本例的编译结果,很明显,GCC为__in2选择了和__out相同的寄存器%eax,这与我们的初衷不符。
main:                              
        pushl   %ebp               
        movl    %esp, %ebp         
        subl    $12, %esp           
        movl    $8, -4(%ebp)        
        movl    $4, -8(%ebp)        
        movl    $3, -12(%ebp)      
        movl    -4(%ebp), %edx      # __in1使用寄存器%edx
        movl    -8(%ebp), %eax      # __in2使用寄存器%eax
#APP                                
        popl %eax                  
        movl %edx, %esi            
        movl %eax, %edi            
                                    
#NO_APP                             
        movl    %eax, %eax         
        movl    %eax, -12(%ebp)     # __out使用寄存器%eax
        movl    $0, %eax            
        leave                       
        ret                        
为了避免这种情况,我们必须向GCC声明这一点,要求GCC为所有的Input操作表达式指定别的寄存器,方法就是在Output操作表达式"=a"(__out)的操作约束中加入&约束,由于GCC规定等号(=)约束必须放在第一个,所以我们写作"=&a"(__out)。
下面是我们将&约束加入之后编译的结果:
main:                          
        pushl   %ebp           
        movl    %esp, %ebp     
        subl    $12, %esp      
        movl    $8, -4(%ebp)   
        movl    $4, -8(%ebp)   
        movl    $3, -12(%ebp)  
        movl    -4(%ebp), %edx    #__in1使用寄存器%edx
        movl    -8(%ebp), %eax
        movl    %eax, %ecx         # __in2使用寄存器%ecx
#APP                           
        popl     %eax              
        movl    %edx, %esi        
        movl    %ecx, %edi        
                              
#NO_APP                        
        movl    %eax, %eax     
        movl    %eax, -12(%ebp)   #__out使用寄存器%eax
        movl    $0, %eax      
        leave                  
        ret                    
OK!这下好了,完全与我们的意图吻合。
如果一个Output操作表达式的寄存器约束被指定为某个寄存器,只有当至少存在一个Input操作表达式的寄存器约束为可选约束时,(可选约束的意思是可以从多个寄存器中选取一个,或使用非寄存器方式),比如"r"或"g"时,此Output操作表达式使用&修饰才有意义。如果你为所有的Input操作表达式指定了固定的寄存器,或使用内存/立即数约束,则此Output操作表达式使用&修饰没有任何意义。比如:
__asm__ ("popl %0 /n/t"           
                 "movl %1, %%esi /n/t"   
                 "movl %2, %%edi /n/t"   
                 : "=&a"(__out)            
                 : "m" (__in1), "c" (__in2));
此例中的Output操作表达式完全没有必要使用&来修饰,因为__in1和__in2都被指定了固定的寄存器,或使用了内存方式,GCC无从选择。
但如果你已经为某个Output操作表达式指定了&修饰,并指定了某个固定的寄存器,你就不能再为任何Input操作表达式指定这个寄存器,否则会出现编译错误。比如:
__asm__ ("popl %0 /n/t"           
                 "movl %1, %%esi /n/t"   
                 "movl %2, %%edi /n/t"   
                 : "=&a"(__out)            
                 : "a" (__in1), "c" (__in2));
本例中,由于__out已经指定了寄存器%eax,同时使用了符号&修饰,则再为__in1指定寄存器%eax就是非法的。

反过来,你也可以为Output指定可选约束,比如"r","g"等,让GCC为其选择到底使用哪个寄存器,还是使用内存方式,GCC在选择的时候,会首先排除掉已经被Input操作表达式使用的所有寄存器,然后在剩下的寄存器中选择,或干脆使用内存方式。比如:
__asm__ ("popl %0 /n/t"           
                 "movl %1, %%esi /n/t"   
                 "movl %2, %%edi /n/t"   
                 : "=&r"(__out)            
                 : "a" (__in1), "c" (__in2));
本例中,由于__out指定了约束"r",即让GCC为其决定使用哪一格寄存器,而寄存器%eax和%ecx已经被__in1和__in2使用,那么GCC在为__out选择的时候,只会在%ebx和%edx中选择。
前3个修饰符只能用在Output操作表达式中,而百分号[%]修饰符恰恰相反,只能用在Input操作表达式中,用于向GCC声明:“当前Input操作表达式中的C/C++表达式可以和下一个Input操作表达式中的C/C++表达式互换”。这个修饰符号一般用于符合交换律运算,比如加(+),乘(*),与(&),或(|)等等。我们看一个例子:
int main(int __argc, char* __argv[])           
{                                             
        int __in1 = 8, __in2 = 4, __out = 3;         
                                               
        __asm__ ("addl %1, %0/n/t"            
                         : "=r"(__out)                  
                         : "%r" (__in1), "0" (__in2));     
                                               
        return 0;                              
}
在此例中,由于指令是一个加法运算,相当于等式__out = __in1 + __in2,而它与等式__out = __in2 + __in1没有什么不同。所以使用百分号修饰,让GCC知道__in1和__in2可以互换,也就是说GCC可以自动将本例的内联汇编改变为:
__asm__ ("addl %1, %0/n/t"
                  : "=r"(__out)
                  : "%r" (__in2), "0" (__in1));     
修饰符 Input/Output 意义
= O 表示此Output操作表达式是Write-Only的
+ O 表示此Output操作表达式是Read-Write的
& O 表示此Output操作表达式独占为其指定的寄存器
% I 表示此Input操作表达式中的C/C++表达式可以和下一个Input操作表达式中的C/C++表达式互换

4. 占位符
什么叫占位符?我们看一看下面这个例子:
__asm__ ("addl %1, %0/n/t"
                  : "=a"(__out)
                  : "m" (__in1), "a" (__in2));
这个例子中的%0和%1就是占位符。每一个占位符对应一个Input/Output操作表达式。我们在之前已经提到,GCC规定一个内联汇编语句最多可以有10个Input/Output操作表达式,然后按照它们被列出的顺序依次赋予编号0到9。对于占位符中的数字而言,和这些编号是对应的。
由于占位符前面使用一个百分号(%),为了区别占位符和寄存器,GCC规定在带有C/C++表达式的内联汇编中,"Instruction List"中直接写出的寄存器前必须使用两个百分号(%%)。
GCC对其进行编译的时候,会将每一个占位符替换为对应的Input/Output操作表达式所指定的寄存器/内存地址/立即数。比如在上例中,占位符%0对应Output操作表达式"=a"(__out),而"=a"(__out)指定的寄存器为%eax,所以把占位符%0替换为%eax,占位符%1对应Input操作表达式"m"(__in1),而"m"(__in1)被指定为内存操作,所以把占位符%1替换为变量__in1的内存地址。
也许有人认为,在上面这个例子中,完全可以不使用%0,而是直接写%%eax,就像这样:
__asm__ ("addl %1, %%eax/n/t"
                  : "=a"(__out)
                  : "m" (__in1), "a" (__in2));
和上面使用占位符%0没有什么不同,那么使用占位符%0就没有什么意义。确实,两者生成的代码完全相同,但这并不意味着这种情况下占位符没有意义。因为如果不使用占位符,那么当有一天你想把变量__out的寄存器约束由a改为b时,那么你也必须将addl指令中的%%eax改为%%ebx,也就是说你需要同时修改两个地方,而如果你使用占位符,你只需要修改一次就够了。另外,如果你不使用占位符,将不利于代码的清晰性。在上例中,如果你使用占位符,那么你一眼就可以得知,addl指令的第二个操作数内容最终会输出到变量__out中;否则,如果你不用占位符,而是直接将addl指令的第2个操作数写为%%eax,那么你需要考虑一下才知道它最终需要输出到变量__out中。这是占位符最粗浅的意义。毕竟在这种情况下,你完全可以不用。
但对于这些情况来说,不用占位符就完全不行了:
首先,我们看一看上例中的第1个Input操作表达式"m"(__in1),它被GCC替换之后,表现为addl address_of_in1, %%eax,__in1的地址是什么?编译时才知道。所以我们完全无法直接在指令中去写出__in1的地址,这时使用占位符,交给GCC在编译时进行替代,就可以解决这个问题。所以这种情况下,我们必须使用占位符。
其次,如果上例中的Output操作表达式"=a"(__out)改为"=r"(__out),那么__out在究竟使用那么寄存器只有到编译时才能通过GCC来决定,既然在我们写代码的时候,我们不知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。
5. Clobber/Modify
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify域声明这些寄存器或内存。
这种情况一般发生在一个寄存器出现在"Instruction List",但却不是由Input/Output操作表达式所指定的,也不是在一些Input/Output操作表达式使用"r","g"约束时由GCC为其选择的,同时此寄存器被"Instruction List"中的指令修改,而这个寄存器只是供当前内联汇编临时使用的情况。比如:
__asm__ ("movl %0, %%ebx" : : "a"(__foo) : "bx");
寄存器%ebx出现在"Instruction List中",并且被movl指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定"bx",以让GCC知道这一点。
因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作表达式使用"r","g"约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。但除此之外,GCC对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以如果你真的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。
在Clobber/Modify域中指定这些寄存器的方法很简单,你只需要将寄存器的名字使用双引号(" ")引起来。如果有多个寄存器需要声明,你需要在任意两个声明之间用逗号隔开。比如:
__asm__ ("movl %0, %%ebx; popl %%ecx" : : "a"(__foo) : "bx", "cx" );
这些串包括:
声明的串 代表的寄存器
"al","ax","eax" %eax
"bl","bx","ebx" %ebx
"cl","cx","ecx" %ecx
"dl","dx","edx" %edx
"si","esi" %esi
"di", "edi" %edi

由上表可以看出,你只需要使用"ax","bx","cx","dx","si","di"就可以了,因为其它的都和它们中的一个是等价的。
如果你在一个内联汇编语句的Clobber/Modify域向GCC声明某个寄存器内容发生了改变,GCC在编译时,如果发现这个被声明的寄存器的内容在此内联汇编语句之后还要继续使用,那么GCC会首先将此寄存器的内容保存起来,然后在此内联汇编语句的相关生成代码之后,再将其内容恢复。我们来看两个例子,然后对比一下它们之间的区别。
这个例子中声明了寄存器%ebx内容发生了改变:
$ cat example7.c
int main(int __argc, char* __argv[])
{                                    
        int in = 8;                  
                                    
        __asm__ ("addl %0, %%ebx"   
                 : /* no output */   
                 : "a" (in) : "bx");
                                    
        return 0;                    
}
$ gcc -O -S example7.c
$ cat example7.s
main:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ebx                       # %ebx内容被保存           
        movl    $8, %eax
#APP
        addl     %eax, %ebx
#NO_APP
        movl    $0, %eax
        movl    (%esp), %ebx          # %ebx内容被恢复
        leave
        ret
下面这个例子的C源码与上一个例子除了没有声明%ebx寄存器发生了改变之外,其它都相同。
$ cat example8.c
int main(int __argc, char* __argv[])
{                                    
        int in = 8;                  
                                    
        __asm__ ("addl %0, %%ebx"   
                 : /* no output */   
                 : "a" (in) );
                                    
        return 0;                    
}
$ gcc -O -S example8.c
$ cat example8.s
main:                     
        pushl   %ebp      
        movl    %esp, %ebp
        movl    $8, %eax   
#APP                       
        addl %eax, %ebx   
#NO_APP                    
        movl    $0, %eax   
        popl    %ebp      
        ret
仔细对比一下example7.s和example8.s,你就会明白在Clobber/Modify域声明一个寄存器的意义。
另外需要注意的是,如果你在Clobber/Modify域声明了一个寄存器,那么这个寄存器将不能再被用做当前内联汇编语句的Input/Output操作表达式的寄存器约束,如果Input/Output操作表达式的寄存器约束被指定为"r"或"g",GCC也不会选择已经被声明在Clobber/Modify中的寄存器。比如:
__asm__ ("movl %0, %%ebx" : : "a"(__foo) : "ax", "bx");
此例中,由于Output操作表达式"a"(__foo)的寄存器约束已经指定了%eax寄存器,那么再在Clobber/Modify域中指定"ax"就是非法的。编译时,GCC会给出编译错误。
除了寄存器的内容会被改变,内存的内容也可以被修改。如果一个内联汇编语句"Instruction List"中的指令对内存进行了修改,或者在此内联汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其Output操作表达式使用"m"约束,这种情况下你需要使用在Clobber/Modify域使用字符串"memory"向GCC声明:“在这里,内存发生了,或可能发生了改变”。例如:
void * memset(void * s, char c, size_t count)
{
     __asm__("cld/n/t"
                     "rep/n/t"
                     "stosb"
                     : /* no output */
                     : "a" (c),"D" (s),"c" (count)
                     : "cx","di","memory");
       return s;
}
此例实现了标准函数库memset,其内联汇编中的stosb对内存进行了改动,而其被修改的内存地址s被指定装入%edi,没有任何Output操作表达式使用了"m"约束,以指定内存地址s处的内容发生了改变。所以在其Clobber/Modify域使用"memory"向GCC声明:内存内容发生了变动。
如果一个内联汇编语句的Clobber/Modify域存在"memory",那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个时候寄存器中的拷贝已经很可能和内存处的内容不一致了。
这只是使用"memory"时,GCC会保证做到的一点,但这并不是全部。因为使用"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语句相关代码。
当一个内联汇编指令中包含影响eflags寄存器中的条件标志(也就是那些Jxx等跳转指令要参考的标志位,比如,进位标志,0标志等),那么需要在Clobber/Modify域中使用"cc"来声明这一点。这些指令包括adc, div,popfl,btr,bts等等,另外,当包含call指令时,由于你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用"cc"。
我很少在相关资料中看到有关"cc"的确切用法,只有一份文档提到了它,但还不是i386平台的,只是说"cc"是处理器平台相关的,并非所有的平台都支持它,但即使在不支持它的平台上,使用它也不会造成编译错误。我做了一些实验,但发现使用"cc"和不使用"cc"所生成的代码没有任何不同。但Linux 2.4的相关代码中用到了它。如果谁知道在i386平台上"cc"的细节,请和我联系。
另外,还可以在Clobber/Modify域指定数字0到9,以声明第n个Input/Output操作表达式所使用的寄存器发生了变化,但正如我们在前面所提到的,如果你为某个Input/Output操作表达式指定了寄存器,或使用"g","r"等约束让GCC为其选择寄存器,GCC已经知道哪个寄存器内容发生了变化,所以这么做没有什么意义;我也作了相关的试验,没有发现使用它会对GCC生成的汇编代码有任何影响,至少在i386平台上是这样。Linux 2.4的所有i386平台相关内联汇编代码中都没有使用这一点,但S390平台相关代码中有用到,但由于我对S390汇编没有任何概念,所以,也不知道这么做的意义何在。

 

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

基于x86的Hello World汇编代码分析(AT&T风格汇编)

转自 http://blog.csdn.net/stephenjy/archive/2009/03/10/3978161.aspx

 

    本文通过对由gcc对简单C语言代码编译生成的汇编码进行逐句分析解读,来学习x86的汇编结构和堆栈机制。文章涉及细节较多,难免出错,望读者不吝赐教!

一、代码

C语言代码:
/* file: hello.c */
 1 #include <stdio.h>
 2
 3 int  add( int  a, int  b){
 4      return  (a+b);
 5 }
 6
 7 int  main( int  argc, char  **argv){
 8      int  a, b, c;
 9     a = 3 ;
10     b = 4 ;
11     c = add(a, b);
12     printf( "a+b= %d /n " , c);
13     printf( "Hello World! /n " );
14      return   0 ;
15 }
16


gcc -S -ohello.s hello.c输出文件:
/* file: hello.s */
 1      .file   " hello . c "
 2      .text
 3 .globl   add
 4      .type    add , @ function
 5 add :
 6      pushl   % ebp
 7      movl    % esp , % ebp
 8      movl    12 (% ebp ), % edx
 9      movl    8 (% ebp ), % eax
10      addl    % edx , % eax
11      popl    % ebp
12      ret
13      .size    add , .- add
14      .section    .rodata
15 .LC 0:
16      .string     " a + b =% d / n "
17 .LC 1 :
18      .string     " Hello   World !"
19      .text
20 .globl   main
21      .type    main , @ function
22 main :
23      leal    4 (% esp ), % ecx
24      andl    $- 16 , % esp
25      pushl   - 4 (% ecx )
26      pushl   % ebp
27      movl    % esp , % ebp
28      pushl   % ecx
29      subl    $ 36 , % esp
30      movl    $ 3 , - 8 (% ebp )
31      movl    $ 4 , - 12 (% ebp )
32      movl    - 12 (% ebp ), % eax
33      movl    % eax , 4 (% esp )
34      movl    - 8 (% ebp ), % eax
35      movl    % eax , (% esp )
36      call    add
37      movl    % eax , - 16 (% ebp )
38      movl    - 16 (% ebp ), % eax
39      movl    % eax , 4 (% esp )
40      movl    $ .LC 0,  (% esp )
41      call    printf
42      movl    $ .LC 1 , (% esp )
43      call    puts
44      movl    $ 0,  % eax
45      addl    $ 36 , % esp
46      popl    % ecx
47      popl    % ebp
48      leal    - 4 (% ecx ), % esp
49      ret
50      .size    main , .- main
51      .ident  " GCC : ( Ubuntu   4 . 3 . 2 - 1 ubuntu12 ) 4 . 3 . 2 "
52      .section    .note.GNU - stack ,"",@ progbits

二、分析


    下面对hello.s进行逐句分析。
    第1行为gcc留下的文件信息;第2行标识下面一段是代码段,第3、4行表示这是add函数的入口,第5行为入口标号;6~12行为add函数体,稍后 分析;13行为add函数的代码段的大小;14行指示下面是数据段;15~18行定义了main中要用到的两个字符串常量;19行同第二行,20、21行 定义了main函数入口,22行为main入口标号。23行开始正式进入main函数,直至49行;50行为main函数代码段体积。51、52行为 gcc留下的信息。
    下面从main函数开始单步分析每一句话,并跟踪堆栈状态。
    初始状态,堆栈状态如 图一
高    +----------------+ <-- esp (栈顶)                          高    +----------------+
 |    |                  |                                                     |    |                  |
 |   +----------------+                                                     |   +    若干      +
 |    |                 |                                                    |    |                 |
 |   +----------------+                                                     |   +----------------+    <-- esp
 |    |                 |                                                    |    |                 |   
 |   +----------------+                                                     |   +----------------+
 V     |                  |                                                      V    |                  |
低    +    ....        +                                                     低   +    ....        +
            图一                                                                               图二

23     leal    4 (% esp ), % ecx
将esp所指地址加4得到的地址存入ecx。
24     andl    $- 16 , % esp  
-16的补码为11...10000,这句话使esp指针下移若干位,新地址末四位是0,
故按16字节对齐,如图二 。对齐是为了加速CPU访存。
25     pushl   - 4 (% ecx )
将ecx所指地址(也就是程序开始时esp所指位置,如 图一 所示)的内容压栈。这个内容是eip。关于这句的用途,后面有详细解释。
26     pushl   % ebp
将ebp压栈,保存ebp的值,以便在退出函数时恢复。
27     movl    % esp , % ebp
将ebp移动到esp的位置。
28     pushl   % ecx
将ecx的值压栈,保存,在退出函数时,通过这个值来恢复esp的初始值。
现在,堆栈状态如图三
高    +----------------+  <-- old esp
 |    |                  |                               
 |   +---- 若干 ----+                               
 |    |                 |    
 |   +----------------+  
 |    |                 |  
 |   +----------------+                                        
 |    |      eip      |       25     pushl   - 4 (% ecx )                            
 |   +----------------+ <-- ebp   27     movl    % esp , % ebp     
 |    |   old ebp  |      26     pushl   % ebp (we don't know what old ebp is, but we have to backup it)
 |   +----------------+ <-- esp
 |    | old esp+4 |        28     pushl   % ecx (ecx =old esp + 4)                               
 |   +----------------+  
 |    |                |  
 |   +----------------+  
 V     |                 |                                                 
低    +    ....        +                                          
            图三
29     subl    $ 36 , % esp
esp向下移动36字节,留出空间给局部变量使用,每个存储单元4字节,故共9格。这里预留的空间有些多,在后续的分析中会发现,很多空都没用上。在第四部分的优化后的代码中也可以看到,36被优化成了20,预留的空间正好用满。
30     movl    $ 3 , - 8 (% ebp )
a = 3, 将a的值存入堆栈(加载到内存中)。
31     movl    $ 4 , - 12 (% ebp )
b = 4, 将b的值存入堆栈(加载到内存中)。
32      movl    - 12 (% ebp ), % eax
33      movl    % eax , 4 (% esp )
将b的值调入寄存器,并且入栈,为调用add函数准备参数。
34      movl    - 8 (% ebp ), % eax
35      movl    % eax , (% esp )
将a的值调入寄存器,并且入栈,为调用add函数准备参数。
36      call    add
调用add函数。注意,在这里,call指令隐含执行了一条push %eip的指令,记录当前代码段执行的位置。
下面进入add函数代码。
 6      pushl   % ebp
将ebp值压栈保存。
 7      movl    % esp , % ebp
移动ebp至当前esp位置。
 8      movl    12 (% ebp ), % edx
 9      movl    8 (% ebp ), % eax
将两个参数加载到寄存器。
10      addl    % edx , % eax
相加,结果存入eax寄存器。
11      popl    % ebp
12      ret
出栈,恢复ebp原来的值,函数返回,结果保存在eax中。注意,在ret指令中隐含执行了pop %eip的指令,从pop出来的eip所指的代码处继续执行。
下面回到main函数中。
37     movl    % eax , - 16 (% ebp )
将函数返回值存入堆栈(内存)。
38      movl    - 16 (% ebp ), % eax
将变量c的值加载到寄存器。(此句冗余,编译时加优化选项可消除)
39      movl    % eax , 4 (% esp )
40      movl    $ .LC 0,  (% esp )
将变量c的值和.LC0的地址存入堆栈,为调用printf函数准备参数。
41      call    printf
调用printf函数,不跟踪分析。
这个过程中堆栈状态如 图四

高    +----------------+  <-- old esp
 |    |                  |                               
 |   +---- 若干 ----+                               
 |    |                 |                                 
 |   +----------------+                                        
 |    |      eip      |                          
 |   +----------------+     
 |    |   old ebp  |     
 |   +----------------+
 |    | old esp+4 |                                   
 |   +----------------+
 |    |                |                    
 |   +----------------+  
 |    |        3      |       30     movl    $ 3 , - 8 (% ebp )            a = 3             
 |   +----------------+
 |    |        4      |       31     movl    $ 4 , - 12 (% ebp )          b = 4
 |   +----------------+  
 |    |        7      |       37      movl    % eax , - 16 (% ebp )      eax中为add函数的返回值。             
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |                |                                 
 |   +----------------+
 |    |     4 / 7    |         33      movl    % eax , 4 (% esp ) / 39      movl    % eax , 4 (% esp )
 |   +----------------+ <-- esp    ( 29     subl    $ 36 , % esp )
 |    |  3 / .LC0 |         35      movl    % eax , (% esp )   / 40      movl    $ .LC 0,  (% esp )
 |   +----------------+
 |    |      eip     |                            
 |   +----------------+ <-- ebp    ( 7      movl    % esp , % ebp )
 |    |     ebp      |            6      pushl   % ebp   
 |   +----------------+  
 |    |                |                                 
 |   +----------------+   
  V    |                |  
低    +    ....        +        
             图四

42      movl    $ .LC 1 , (% esp )
将.LC1地址存入堆栈,注意,这里gcc将printf“偷换”成了puts,所以只传一个参数。
43      call    puts
调用puts函数。
44      movl    $ 0,  % eax
主函数将要返回0,将0存入eax寄存器。
45      addl    $ 36 , % esp
将esp回到函数开始时的位置。
46      popl    % ecx
47      popl    % ebp
48      leal    - 4 (% ecx ), % esp
这三句与程序开始正好相对,恢复寄存器状态到进入函数前的状态。开始的这句话: 25  pushl   - 4 (% ecx ),存入了esp初始时刻指向单元的内容(应该是eip),但整个程序中都没用上。
49      ret
从main函数返回,返回值由eax带回。图五是图三的拷贝,可以从此图看清楚备份了哪些东西。

高    +----------------+  <-- old esp
 |    |                  |                               
 |   +---- 若干 ----+                               
 |    |                 |                                 
 |   +----------------+                                        
 |    |      eip      |           
 |   +----------------+     
 |    |   old ebp  |   
 |   +----------------+
 |    | old esp+4 |            
 |   +----------------+
 |    |                 |                                 
 |   +----------------+  
 V     |                  |                                                 
低    +    ....        +                                      
            图五
         

三、总结


    分析完这简单的代码后,我们进行一些小小的总结。
    1、我们体会一些x86是如何使用堆栈的。堆栈是个动态的空间,在运行的过程中,其中保存的内容主要有两种:局部变量和堆栈转移时保存的指针(寄存器的值)。
    2、esp是栈顶指针,pop和push操作将会自动调整esp的值,其他操作,除非esp作为算术运算的结果寄存器外,esp不会改变。个人觉得这里堆 栈称之为堆栈有一点点不合理,因为对堆栈的操作并不是完全的pop/push操作的集合,更多的时候是直接通过地址来取数。发生函数调用 时,4(%esp)是第一个参数,8(%esp)是第二个参数,依此类推,注意,这里加的4,是隐含指令push %eip导致的。push的操作,首先将esp向低地址方向移动4位,然后在这个单元里存入数据;pop的操作,现从esp所指向的单元里取出数据到指定 寄存器,然后将esp向高地址方向移动4位。
    3、一个代码段(这里一个函数就是一个代码段)运行时使用堆栈空间中连续的空间,ebp总是指向当前运行中的函数的堆栈空间的第一个位置,也就是基地址的 意思。一个代码段在存取自己所使用的数据时总是通过ebp来索引,而获取参数总是通过esp索引。所以在进入一个函数时,必须保存ebp的值,然后将 ebp指向自己的数据其实地址,在退出函数时,恢复ebp的值,使调用它的函数在它返回后能继续正常运行。在main函数开始时改变了esp的值,所以改 变之前也需要备份esp的值。
    4、函数返回值默认存放在eax寄存器中。
    5、寻址方法:
  • 立即数寻址:$num,num为数值,也就是字面数值
  • 寄存器寻址:%reg,reg为任一寄存器,取出%reg中保存的值
  • 寄存器间址:disp(base, index, scale),取出 (disp+base+index*scale) 所表示的内存单元中保存的内容。disp, index, scale都可以省略。

    6、main函数中为何要按16字节对齐esp?Linux下面GCC默认的堆栈是16字节对齐的,而这样对齐是为了加快CPU访问效率。这里,不对esp进行16字节对齐并不会影响程序的正确执行。具体的解释参见瀚海xhacker的文章:
http://202.38.64.3/cgi/bbscon?bn=ASM&fn=M47918B7C&num=2388
      7、 25     pushl   - 4 (% ecx )的作用。(以下解释摘自瀚海foxman和xhacker的帖子)

*** foxman ***
一般来说这不是必需的,当进入一个函数之后,堆栈是这样的

|返回地址 |
|old_ebp |  <-  ebp
|  var1    |
|  var2    |
|  var3    |

也就是说在一个函数内部,是根据(ebp+4)来找到这个函数返回地址的。

不过对于main函数,进入之后需要堆栈16字节对齐(即andl $-16, %esp),这样就在原
来的main返回地址,与old_ebp之间插入了一些padding字节。为了还能ebp找到main的返
回地址,所以这儿再一次将main的返回地址入栈pushl -4(%ecx),在栈里放置在old_ebp
上方,如下:

| main返回地址 |
| 填充               |
| 填充               |
| main返回地址 |
| old_ebp          |  <- ebp
| ...                   |

一般gcc就是这么做的。 这么做主要是为了gcc扩展__builtin_return_address.

__builtin_return_address(LEVEL) 返回当前函数或其调用者的返回地址,参数LEVEL
 指定在栈上搜索框架的个数,0 表示当前函数的返回地址,1 表示当前函数的调用
者所在函数的返回地址,依此类推。

这就是根据%ebp来找到返回地址的。

为了能使用__builtin_return_address(0),就需要在push %ebp之前将main返回地
址入栈。如果你不用它,那就没什么问题

*** xhacker ***
另外再加上这个gcc的这个参数
-mpreferred-stack-boundary=x
x=2,3,4 etc,表示栈要2^x字节对齐

cc -mpreferred-stack-boundary=2 -S aa.c
可以看出此时没有那句push -4(%ecx)了,说明正是因为main的对齐,而为了仍然支持
__builtin_return_address扩展加上这条push指令了

***************


四、编译器优化后的代码

gcc -O3 -S -ohello_O3.s hello.c输出文件:
/* file: hello_O3.s */
 1      .file   " hello . c "
 2      .text
 3     . p2align   4 ,, 15
 4 .globl   add
 5      .type    add , @ function
 6 add :
 7      pushl   % ebp
 8      movl    % esp , % ebp
 9      movl    12 (% ebp ), % eax
10      addl    8 (% ebp ), % eax
11      popl    % ebp
12      ret
13      .size    add , .- add
14      .section    .rodata.str 1 . 1 ," aMS ",@ progbits , 1
15 .LC 0:
16      .string     " a + b =% d / n "
17 .LC 1 :
18      .string     " Hello   World !"
19      .text
20     . p2align   4 ,, 15
21 .globl   main
22      .type    main , @ function
23 main :
24      leal    4 (% esp ), % ecx
25      andl    $- 16 , % esp
26      pushl   - 4 (% ecx )
27      pushl   % ebp
28      movl    % esp , % ebp
29      pushl   % ecx
30      subl    $ 20 , % esp
31      movl    $ 7 , 8 (% esp )
32      movl    $ .LC 0,   4 (% esp )
33      movl    $ 1 , (% esp )
34      call    __printf_chk
35      movl    $ .LC 1 , (% esp )
36      call    puts
37      addl    $ 20 , % esp
38      xorl    % eax , % eax
39      popl    % ecx
40      popl    % ebp
41      leal    - 4 (% ecx ), % esp
42      ret
43      .size    main , .- main
44      .ident  " GCC : ( Ubuntu   4 . 3 . 2 - 1 ubuntu12 ) 4 . 3 . 2 "
45      .section    .note.GNU - stack ,"",@ progbits
    从代码中,我们看到add函数虽然得到了相应的代码,但并没有被调用,而c=a+b则直接在编译时计算出了其值:7!其它地方并没有太多的优化。函数调用时相应的保存寄存器状态/返回时恢复等结构化的操作都没有改变。

五、进一步讨论


    main函数的参数argc,argv是如何传递的?看下面的代码:
/* t.c */
 1 #include <stdio.h>
 2
 3 int  main( int  argc, char  **argv){
 4      char  *c;
 5      if (argc == 1 )
 6          return   1 ;
 7      else {
 8         c = argv[ 1 ];
 9         puts(c);
10     }
11      return   0 ;
12 }
13

gcc -S -ot.s t.c的输出文件:
/* g.s */
 1      .file   " hello . c "
 2      .text
 3 .globl   main
 4      .type    main , @ function
 5 main :
 6      leal    4 (% esp ), % ecx
 7      andl    $- 16 , % esp
 8      pushl   - 4 (% ecx )
 9      pushl   % ebp
10      movl    % esp , % ebp
11      pushl   % ecx
12      subl    $ 36 , % esp
13      movl    % ecx , - 28 (% ebp )
14      movl    - 28 (% ebp ), % eax
15      cmpl    $ 1 , (% eax )
16      jne     . L2
17      movl    $ 1 , - 24 (% ebp )
18      jmp     . L3
19 . L2 :
20      movl    - 28 (% ebp ), % edx
21      movl    4 (% edx ), % eax
22      addl    $ 4 , % eax
23      movl    (% eax ), % eax
24      movl    % eax , - 8 (% ebp )
25      movl    - 8 (% ebp ), % eax
26      movl    % eax , (% esp )
27      call    puts
28      movl    $ 0,  - 24 (% ebp )
29 . L3 :
30      movl    - 24 (% ebp ), % eax
31      addl    $ 36 , % esp
32      popl    % ecx
33      popl    % ebp
34      leal    - 4 (% ecx ), % esp
35      ret
36      .size    main , .- main
37      .ident  " GCC : ( Ubuntu   4 . 3 . 2 - 1 ubuntu12 ) 4 . 3 . 2 "
38      .section    .note.GNU - stack ,"",@ progbits

这里面,第6~12行与之前相同,备份寄存器,移动esp,为代码段预留数据空间。执行完这一段后,这里,%ecx是一个“指针”,指向%esp+4的位置,也就是存放argc的位置。(注意,这里的指针不完全同于C语言中指针的概念,这里的指针是指 某寄存器 的值是一个内存单元的地址,C语言中,指针是指 某变量 的值是一个内存单元的地址。)
13      movl    % ecx , - 28 (% ebp )
将ecx这个“指针”复制到堆栈。
14      movl    - 28 (% ebp ), % eax
再把这个“指针”加载到寄存器。
15      cmpl    $ 1 , (% eax )
注意,因为%eax中存放的是“指针”,所以这里有括号。(%eax)即为初始时刻的4(%esp)。
16      jne     . L2
比较,如果argc!=1,跳转到.L2处。
17      movl    $ 1 , - 24 (% ebp )
18      jmp     . L3
如果相等,将main函数欲返回的值存到堆栈中,并且跳转到.L3。
下面看.L2的内容:
20      movl    - 28 (% ebp ), % edx
注意,这里-28(%ebp)是指向存放argc单元的“指针”。
21      movl    4 (% edx ), % eax
再将这个指针向上移动4字节,取出其中的值,即为argv的地址,更准确的说是argv[0]的地址。argv在C语言中是char**型指针。也就是说,%eax-->argv[0],(%eax)==argv[0]
22      addl    $ 4 , % eax
%eax(argv[0]的地址)是一个内存地址,加4后就变成argv[1]的地址。(%eax)==argv[1]
23      movl    (% eax ), % eax
再将这个地址的内容加载到%eax,此时%eax=argv[1]。
24      movl    % eax , - 8 (% ebp )
注意,这里%eax外面没有括号,所以复制的是argv[1],也就是一个char*型的参数。
25      movl    - 8 (% ebp ), % eax
将参数加载到寄存器,这句话有些冗余,优化后会被去除。
26      movl    % eax , (% esp )
为puts准备参数。
27      call    puts
28      movl    $ 0,  - 24 (% ebp )
从puts返回后,准备该分支main函数的返回值,0。可以看到,保存这个返回值的地方同17行。这样,无论从哪个分支出来,都可以直接返回- 24 (% ebp )的内容。
L3则是函数的一些扫尾工作,不需要再分析了。


六、实践


    使用工具Insight跟踪运行hello和t两个文件(gcc -g -o hello hello.s,注意加上-g参数,可以保留汇编代码中的符号信息),观察call语句时push入栈的eip,观察数据段、代码段、堆栈段的地址,观察 各寄存器值的变化,体会几种寻址方法。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值