2012年4月15日星期日

某个C程序的汇编代码解释

偶然看到Allan Ruin大大的某post,原意大概是:子函数内的局部变量没有初始化的话,其值应该是任意的,函数返回后会释放内存。但是为什么第二次调用的时候i的值会是777呢?之前没遇到过类似问题,毕竟是现实中不会出现的情况。但通过汇编代码大概可以研究一下的吧?
这段时间刚好在看CSAPP的GAS语法汇编,恰好拿来练手的说。


C代码如下: 版权属于Allan Ruin :)
#include <stdio.h>
void foo(void)
{
int i;
printf("%d\n", i);
i = 777;
}
int main(void)
{
foo();
foo();
return 0;
}


ouput:
3
777


gcc -s lg_test.c

汇编代码如下



.file "lg_test.c"
.section .rodata             ;read only data segment
.LC0:
.string "%d\n"               ;printf() function's first parameter
.text
.globl foo
.type foo,@function


foo:
1  pushl %ebp              ;save old  %ebp
2  movl %esp, %ebp         ;new %ebp point to old %ebp (i.e. frame pointer)
3  subl $40, %esp          ;allocate 40 bytes on stack
4  movl $.LC0, %eax        ;move the string"%d\n" to %eax
5  movl -12(%ebp), %edx    ;use -12(%ebp) as i's value and save in %edx
6  movl %edx, 4(%esp)      ;set -12(%ebp) as the second parameter,push in stack
7  movl %eax, (%esp)       ;set  %eax( i.e. "%d\n" ) as the first parameter
8  call printf             ;call printf function(you can assume that 
                           ;it doesn't affect the existing stack frame)
9  movl $777, -12(%ebp)    ;NOTE:  -12 (%ebp) has changed
10 leave                   ;i.e.  movl %ebp, %esp ;    pop %ebp ;      
11 ret                     ;return    i.e.  pop %eip ;



.size foo, .-foo
.globl main
.type main,@function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
call foo
call foo
movl $0, %eax              ;return 0
movl %ebp, %esp
popl %ebp
ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits



说说思路:
从main函数开始,保存前一个栈帧的帧指针,然后andl $-16, %esp 这一句不明何解。-16补码是0xfffffff1,按位操作有什么作用?。。略过,调用foo(),也就是push %eip(下一条指令地址入栈),然后jmp到foo函数地址处。
对应行号:
1,2行 保存上一帧的帧指针,移动新的帧指针%ebp指向旧的帧指针,并以之为当前帧的基址。
3行 据书上说是为了提高缓存性能。
4行 将.LC0标号处的字符串送入eax寄存器。
5行 将ebp基址偏移12字节处的4字节内容送入edx寄存器(作为i)。原先我们不知道这个地址存放的是什么内容
6,7行 先将edx(即i)入栈,再将eax(即"%d\n")入栈。这是按照printf()函数参数的反序入栈,调用printf的时候需要用到。
8行 call printf。照例先将eip入栈,然后ebp入栈,读取参数很自然就是 8(%ebp) 多为第一个参数,12(%ebp) 作为第二个参数。接着就是printf细节。。。。
9行 将777送入i所在的内存地址处,即 -12(%ebp) 。
10行 leave做返栈准备。也就是按1,2行的相反顺序,esp回到原来的位置,再把旧的ebp弹栈恢复。
11行 ret,pop出eip的值,返回main进程。

这样mian函数继续执行,同时注意到,main函数栈帧数据是没有变化的。也就是说,调用foo()函数并没有修改到main函数的栈帧数据。因此调用第二个foo()函数的过程跟第一个完全一样。但是,当第五行读入 -12(%ebp)数据到eax的时候,不再是未知的值,而是第一次调用的时候,写入到该位置的777。因此printf输入的是777。主要是因为弹栈的过程并不会修改栈的数据,故777得以保留。


稍微修改一下源程序,检验一下:
#include<stdio.h>
void showbyte(unsigned char* start,int len){
int i;
for(i = 0;i < len;i ++)printf("%.2x",start[i]);
printf("\n");
}

void foo(){
int i;
printf("%d\n",i);
int* p = &i;
showbyte((unsigned char*)&p,sizeof(p));   //输出i变量的地址
i = 777;
}

int main(){
foo();
foo();
return 0;
}

output:

3
5c40a7bf    
777
5c40a7bf      //显示两次函数调用,i的地址都是一样的


其实我觉得用UML图来说明栈机制和内容变化要比文字说明来得更清晰明了。但是现在貌似条件不允许。。。


个人感悟:以前看王爽的《汇编语言》,学习的是80X86的16位汇编,intel语法。现在看来,确实只能算是汇编入门。霎时间换到32位的GAS语法,稍显不适应。。。但是无可否认,32位的汇编语法机制要比16位完善很多,也复杂很多。各种细节问题,要记的不少。综合起来,计算机底层知识也是环环相扣,相辅相成。无论哪方面理解的不够深刻,都会成为个人能力的短板。了解程序的机器级表示,确实对提高程序效率有莫大帮助。

没有评论:

发表评论