一 AT&T汇编(Linux使用的汇编格式)
基于自己已经熟悉的x86汇编,这里只介绍一个x86于AT&T之间的区别 ;
1. 大小写
INTEL格式的指令使用大写字母,而AT&T格式的使用小写字母。
例: INTEL AT&T
MOV EAX,EBX movl %ebx,%eax
2.操作数赋值方向
在INTEL语法中,第一个表示目的操作数,第二个表示源操作数,赋值方向从右向左。
AT&T语法第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然。
3. 前缀
在 INTEL 语法中寄存器和立即数不需要前缀;AT&T 中寄存器需要加前缀“%” ;立即数需要加前缀“$” 。
4.后缀
AT&T 语法中大部分指令操作码的最后一个字母表示操作数大小, “b”表示 byte(一个字节) ;“w”表示 word(2 个字节) ;“l”表示 long(4 个字节)。
5.间接寻址
INTEL 中基地址使用“[” 、“]” ,而在 AT&T 中使用“(”、“)” 。
二 x86CPU寄存器(32位)

其中:32位寄存器前面多加了一个E,表示该寄存器是32位的。
♦CS——代码段寄存器(Code Segment Register),其值为代码段的段值;
♦DS——数据段寄存器(Data Segment Register),其值为数据段的段值;
♦ES——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
♦SS——堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值;
♦FS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
♦GS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值。
其中,cs:eip准确定位一个指令的地址位置;
三 几个简单指令的解析
pushl %eax subl $4, %esp movl %eax,(%esp)
popl %eax movl (%esp),%eax addl $4,%esp
call 0x12345 pushl %eip(*) movl $0x12345,%eip
ret popl %eip(*)
注:由于eip寄存器不能直接操作,所以对eip的操作都是伪指令,伪指令用(*)表示;
enter pushl %ebp movl %esp,%ebp
leave movl %ebp,%esp popl %ebp
四 linux下GCC基本教程
这里给出google的链接ftp://wcmc.csu.edu.cn/released_by_others/jasny/linux%E7%9B%B8%E5%85%B3/linux%E4%B8%8Bgcc%E5%9F%BA%E6%9C%AC%E6%95%99%E7%A8%8B.pdf(不知道能不能打开);
虽然我们称 gcc 是 C 语言的编译器,但使用 gcc 由 C 语言源代码文件生成可执行文件的过程不仅仅是编译的过程,而是要经历四个相互关联的步骤∶预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编(Assembly)和连接(Linking)。
一般来讲,.S为后缀的汇编语言源代码文件和汇编、.s为后缀的汇编语言文件经过预编译和汇编之后都生成以.o为后缀的目标文件。
gcc最基本的用法是∶gcc [options] [filenames]
其中options就是编译器所需要的参数,filenames给出相关的文件名称。
-c,只编译,不连接成为可执行文件,编译器只是由输入的.c等源代码文件生成.o为后缀的目标文件,通常用于编译不包含主程序的子程序文件;
-o output_filename,确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就给出预设的可执行文件a.out;
-Wall, 编译警告选项,在编译的过程中如果gcc遇到一些它认为可能会发生错误的地方就会提出一些相应的警告和提示信息。提示我们注意这个地方是不是有什么失误导致的错误。
五 一个简单C代码的汇编程序
原C语言代码:
1 #include<stdio.h> 2 int g(int x) 3 { 4 return x+3; 5 } 6 7 int f(int x) 8 { 9 return g(x)+3; 10 } 11 12 int main(void) 13 { 14 printf("%d\n",f(8)+3); 15 }
通过gcc -S -o main.s main.c -m32 生成汇编代码(-m32表示生产的是32位的代码)
.file "main.c" .text .globl g .type g, @function g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $3, %eax popl %ebp ret .size g, .-g .globl f .type f, @function f: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g addl $3, %eax leave ret .size f, .-f .section .rodata .LC0: .string "%d\n" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp movl $8, (%esp) call f leal 3(%eax), %edx movl $.LC0, %eax movl %edx, 4(%esp) movl %eax, (%esp) call printf leave ret .size main, .-main .ident "GCC: (Ubuntu/Linaro 4.4.7-1ubuntu2) 4.4.7" .section .note.GNU-stack,"",@progbits
由于自动生产的汇编文件有一些链接用的辅助信息,去掉以“.”开头的部分;获得干净的汇编代码;
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $3, %eax popl %ebp ret f: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g addl $3, %eax leave ret main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $16, %esp movl $8, (%esp) call f leal 3(%eax), %edx movl $.LC0, %eax movl %edx, 4(%esp) movl %eax, (%esp) call printf leave ret
分析该部分汇编代码:
程序从main开始执行,
将当前ebp的值入栈(保存当前函数栈的栈底位置),然后将当前栈顶和栈底都只想当前栈顶位置(相当于新建了一个栈,即main函数的栈),
然后立即数8入栈;
然后调用f函数(即将当前位置的eip入栈,然后将f函数的地址赋值给eip),
将当前ebp的值入栈(保存当前函数栈的栈底位置),然后将当前栈顶和栈底都只想当前栈顶位置(建立了一个f函数的栈),
然后通过8(%ebp)找到上个地址中刚刚存放立即数的位置,找到立即数8,并赋值给eax寄存器(movl 8(%ebp), %eax),
eax寄存器入栈,调用g函数;
在g函数中,开始任然是将当前ebp的值入栈(保存当前函数栈的栈底位置),然后将当前栈顶和栈底都只想当前栈顶位置(建立了一个g函数的栈),
然后用同样的方法找到栈中f函数中给eax寄存器的值(也是立即数的值,8),然后更新eax寄存器,并给eax寄存器实现加3的操作。
调用ret 命令,返回到f函数;即开始执行 addl $3, %eax 这句汇编代码,实现加3;
执行leave命令,返回到main函数的函数栈中;
调用ret,返回到leal 3(%eax), %edx处,将eax的值再次加3,辅助给edx;
然后将 LC0 段的基地址传到 eax 寄存器中(.LC0是个标签,用于寄存器传参),
然后调用C库中的函数printf实现标准输出。
leave ret返回刚上一级,即退出当前的main函数。
邵学锋
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
2017/2/22
浙公网安备 33010602011771号