Linux下简单C语⾔⼩程序的反汇编分析
韩洋
原创作品转载请注明出处
《Linux内核分析》MOOC课程
写在开始,本⽂为因为参加MOOC相关课程⽽写的作业,如有疏漏,还请指出。
选了⼀门Linux内核分析课程,因为阅读内核代码中或多或少要涉及到At&T汇编代码的阅读,所以这⾥写下⼀个对⼀个简单C命令⾏程序的反汇编分析过程,⼀⽅⾯完成作业,另⼀⽅⾯当作练⼿。下⾯开始:
1、编写我们的C语⾔⼩程序
这⾥我们使⽤简单的例⼦,代码如下:
1 #include <stdio.h>
2
3int exG(int x)
4 {
5return x + 5;
linux下vim命令6 }
7
8int exF(int x)
9 {
10return exG(x);
11 }
12
13int main(void)
14 {
15return exF(10) + 2;
16 }
使⽤vim等编辑器写⼊上述代码,保存到main.c,然后使⽤下⾯命令⽣成汇编源⽂件:
x86系统:
$gcc -S -o main.s main.c
x64系统:
$gcc -m32 -S -o main.s main.c
因为我们这⾥以32位平台为例⼦,所以在x64机器上要加上-m32来使GCC⽣成32位的汇编源⽂件。
2、处理源⽂件
执⾏完上述命令后,当前⽬录下就会有⼀个main.s的⽂件,使⽤vim打开,不需要的链接信息[以"."开头的⾏],得到如下汇编代码:
1 exG:
2 pushl %ebp
3 movl %esp, %ebp
4 movl 8(%ebp), %eax
5 addl $5, %eax
6 popl %ebp
7 ret
8 exF:
9 pushl %ebp
10 movl %esp, %ebp
11 pushl 8(%ebp)
12 call exG
13 addl $4, %esp
14 leave
15 ret
16 main:
17 pushl %ebp
18 movl %esp, %ebp
19 pushl $10
20 call exF
21 addl $4, %esp
22 addl $2, %eax
23 leave
24 ret
可以看到这个⽂件⾥是GCC帮我们⽣成的汇编代码,这⾥需要说明下AT&T格式和intel格式,这两种格式GCC是都可以⽣成的,如果要⽣成intel格式的汇编代码,只需要加上 -masm=intel选项即可,但是Linux下默认是使⽤AT&T格式来书写汇编代码,Linux Kernel代码中也是AT&T格式,我们要慢慢习惯使⽤AT&T格式书写汇编代码。这⾥最需要注意的AT&T和intel汇编格式不同点是:
AT&T格式的汇编指令是“源操作数在前,⽬的操作数在后”,⽽intel格式是反过来的,即如下:
AT&T格式:movl %eax, %edx
Intel格式:mov edx, eax
表⽰同⼀个意思,即把eax寄存器的内容放⼊edx寄存器。这⾥需要注意的是AT&T格式的movl⾥的l表⽰指令的操作数都是32位,类似的还是有movb,movw,movq,分别表⽰8位,16位和64位的操作数。更具体的AT&T汇编语法请执⾏Google或者查阅相关书籍。
3、汇编代码分析
下⾯开始分析汇编代码,运⾏程序后,C Runtime会在进⾏⼀系列准备⼯作后把我们让eip指向我们的main函数开始执⾏,所以这⾥从main 开始分析:
⾸先进⼊gdb调试环境:
在我们的机器上输⼊如下命令⽣成带有调试信息的elf⽂件,然后进⼊gdb进⾏调试:
$gcc -m32 -g -o main main.c
$gdb main -tui -q
进⼊gdb后,输⼊layout asm切换到反汇编视图,同时在main函数处下断点:
(gdb)layout asm
(gdb)b main
然后我们使⽤
(gdb)si
来逐条指令执⾏并观察寄存器变化情况,如图:
对于main函数:
逐条指令执⾏
(gdb)si
pushl %ebp
movl %esp, %ebp
...
这两条是Prolog,其作⽤包含保存当前的栈环境,以确保函数能正确返回和为当前函数开辟新的栈空间。这两句的执⾏效果是把当前的ebp 值⼊栈,再把ebp⼊栈后的esp中的值放⼊ebp。此时,esp和ebp都指向同⼀个内存地址。
这⾥需要说明的是⼊栈和出栈操作,在intel的x86架构上,栈是从⾼地址向低地址增长,所以:
⼊栈等价于:1、esp先下移留出对应的空间;2、把相应数值放⼊刚刚留出的空间完成⼊栈
出栈等价于:1、从当前esp指向内存取出数值;2、esp向上移动,释放相应空间
此时栈中的情况如下如所⽰:[从这⾥开始,下图中每个空格皆表⽰4字节内存空间]
图1
继续逐条指令执⾏
1 pushl $10
2 call exF
3 addl $4, %esp
4 ....
pushl $10,当前esp先减4,然后把宽度为4直接的数值10放⼊esp当前指向的内存中。
call exF ,函数调⽤指令,⾸先把当前eip的值[当前eip指向第三条指令,即addl $4, %esp]⼊栈,然后跳转到exF函数的第⼀条指令开始执⾏。
此时栈中的情况如下如所⽰:
图2
对于exF函数:
逐条指令执⾏
(gdb)si
1 pushl %ebp
2 movl %esp, %ebp
3 pushl 8(%ebp)
4 call exG
5 addl $4, %esp
6 ....
这⾥前条指令和main函数的头两条指令作⽤相同,保存当前栈环境,为exF函数开辟新的栈空间
pushl 8(%ebp),该指令把当前ebp中的数值加8后作为内存地址,并把该内存地址指向的内存空间内的数值"10"放⼊栈中。[参考图2可以发现其实就是把调⽤函数是传⼊的参数⼊栈]
call exG,函数调⽤指令,当前eip⼊栈后,跳转到exG函数的第⼀条指令执⾏。
此时栈中的情况如下如所⽰:
图3
对于exG函数:
逐条指令执⾏
(gdb)si
1 pushl %ebp
2 movl %esp, %ebp
3 movl 8(%ebp), %eax
4 addl $5, %eax
5 popl %ebp
6 ret
⾸先依然是函数前⾔(Prolog),保存栈环境,开辟新的栈空间
此时栈中的情况如下如所⽰:
图4
此时GDB⾥使⽤bt 查看运⾏栈情况如下图:
movl 8(%ebp),%eax 该指令把当前ebp中的数值加8后作为内存地址,并把该内存地址指向的内存空间内的数值“10”放⼊eax寄存器中。[参照图4可以发现就是把调⽤函数是传⼊的参数放⼊eax寄存器]
addl $5, %eax AT&T汇编语⾔中$符号后⾯跟上数字表⽰⼀个⽴即数,这⾥即为把eax中的值加上5,再放回eax,此时eax的值为15.
popl %ebp,从栈中获取旧的esp值,并放⼊ebp寄存器。[这⾥之所以没有再加上⼀条movl %ebp, %esp是因为函数中esp的值并没有改变,依然指向存放旧esp值的内存空间]
ret 等价于pop eip,从当前栈顶,即esp所指内存处获取值,作为eip,然后跳转到eip中存放的地址继续执⾏。
此时栈中情况如图:
图5
到这⾥,函数exG已经返回,其返回值存储在eax寄存器中,即返回值为15
返回到函数exF中
1 ...
2 addl $4, %esp
3 leave
4 ret
程序从上述指令开始继续执⾏,
addl $4, %esp 回收栈空间,栈空间收缩4个字节,
leave,等价于如下两条指令
movl %ebp, %esp
pop %ebp
即函数结语[EpiLog],释放exF函数使⽤的栈空间,此时栈中情况如图:
图6
再接着是ret指令,该指令执⾏后,函数exF返回,程序回到main函数继续执⾏,此时栈中情况如图:
图7
此时eax中存放的是函数exF的返回值,即15
回到main函数继续执⾏
1 ...
2 addl $4, %esp
3 addl $2, %eax
4 leave
5 ret
addl $4, %esp 栈收缩4个字节,回收栈空间
addl $2, %eax 此时eax中的值是main函数调⽤函数exF的得到的返回值,即15,本条指令将eax中的值加2后放回eax,执⾏后eax中的值为17 leave 函数结语,本条指令执⾏后,ebp的值为图7中⿊⾊Old EBP表⽰的值,esp指向图7中⿊⾊Old ebp所在内存空间的上⼀个内存空间,该处存放的是指向CRT调⽤main函数后紧接的指令的所在的内存地址
ret main函数返回
4、总结
计算机⼯作的过程实际上就是“取指令,执⾏指令”的循环,程序在执⾏时被装⼊内存,计算机从内存中某个位置开始读取指令按照⼀定逻辑顺序执⾏,直到程序结束。在执⾏过程中根据需要为程序中各个模块在内存中开辟⼀定的空间[如栈,堆],运⾏栈对应函数调⽤⼗分重要,函数参数和⾃动变量都存储于运⾏栈中。计算机从内存的什么地⽅开始执⾏指令完全由cpu中指令指针寄存器[EIP]中的值决定,并不会区分内存中什么地⽅是代码段,什么地⽅是数据段。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论