Chinaunix首页 | 论坛 | 博客
  • 博客访问: 228163
  • 博文数量: 93
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 542
  • 用 户 组: 普通用户
  • 注册时间: 2014-12-09 16:59
文章分类

全部博文(93)

文章存档

2016年(27)

2015年(66)

我的朋友

分类: C/C++

2015-08-27 23:57:43

    前几天读到一篇关于堆栈的文章,感觉对栈的理解很重要,有必要把栈的相关知识再复习,总结,归纳一下。然后就有了下面的学习历程
    
    函数的调用过程中需要用到栈(C语言默认的调用规则,也有用到寄存器传递参数的情况,后面有说明)来传递参数,保存返回地址,非静态局部变量的内存分配也分配在栈里面。

以下为我在学习过程中的实例分析(编译环境为Fedora12下的GCC):

C代码:

#include

void print(void)
{
    unsigned int _ebp;
    int i;
    __asm__ __volatile__ ("movl %%ebp, %0":"=a"(_ebp));    //这里先是得到print函数的栈底指针,保存在变量_ebp中
    int *p=(int *)((*(unsigned int *)_ebp)-4-4-4-4-8-7*4-8);  
    //(*(int *)_ebp)这样就得到了上一个函数即main函数的栈底指针,因为函数栈底所在地址保存的值即上一次函数的栈底指针
   
    for(i=0;i<7;i++)
       printf("%d\n", p[i]);
}

int main()

 int s=0;
 int ss=0;
 int sss = 100;
 char fdsa='f';
 char srt[8];
 int arr[]={32,43,3,567,987,21,56};
 print();
return 0;
}

gcc -S -o main.s main.c得到汇编代码:

.file "main5.c"
.section .rodata

.LC0:    #常量区
.string "%d\n"    #printf函数格式化字符串

.text
.globl print    #print函数
.type print, @function
print:
pushl %ebp
movl %esp, %ebp
subl $40, %esp    #以上三句main函数里面有类似的分析,我是从main函数开始分析的
movl $0, -16(%ebp)    #相当于C代码里的j = 0;

#APP
# 10 "main5.c" 1
movl %ebp, %eax
# 0 "" 2
#NO_APP

movl %eax, -24(%ebp)
movl -24(%ebp), %eax
movl (%eax), %eax
subl $60, %eax    #以上四句获取上一函数帧即main函数帧的栈底,并减去相应值,指向特定的内存地址

movl %eax, -12(%ebp)    #相当于C代码里的给p指针赋值
movl $0, -20(%ebp)    #i = 0;
jmp .L2    #跳转指令

.L3:
movl -20(%ebp), %eax    #i的值赋给寄存器eax
sall $2, %eax    #左移两位,即乘4,因为整形是4个字节的
addl -12(%ebp), %eax    #指向相应数据的地址
movl (%eax), %edx    #获取数据给寄存器edx
movl $.LC0, %eax    #获取格式化字符串给寄存器eax
movl %edx, 4(%esp)    #以下两句为printf函数参数入栈
movl %eax, (%esp)
call printf    #调用printf函数
addl $1, -20(%ebp)    #i++;

.L2:
cmpl $6, -20(%ebp)    #判断i的值是否小于等于6
jle .L3

leave
ret
.size print, .-print

.globl main    //main函数
.type main, @function
main:
pushl %ebp    #保存上一次函数帧的栈底
movl %esp, %ebp    #ebp指向main函数帧的栈底
andl $-16, %esp    #使栈顶指针十六字节对齐,即其值为十六的倍数
subl $64, %esp    #栈顶下移64个字节,给main函数提供栈空间
movl $0, 48(%esp)    #以下至call print 前一句为main函数局部变量入栈
movl $0, 52(%esp)
movl $100, 56(%esp)
movb $102, 63(%esp)
movl $32, 12(%esp)
movl $43, 16(%esp)
movl $3, 20(%esp)
movl $567, 24(%esp)
movl $987, 28(%esp)
movl $21, 32(%esp)
movl $56, 36(%esp)
call print    #调用print函数,这里包含两个动作,返回地址入栈和IP指针指向print函数地址
movl $0, %eax    #返回值0赋给寄存器eax,ret指令执行时用到
leave    #相当于movl %ebp, %esp    popl %ebp
ret    #返回指令
.size main, .-main
.ident "GCC: (GNU) 4.4.2 20091027 (Red Hat 4.4.2-7)"
.section .note.GNU-stack,"",@progbits


在这里要特别指出的是不同的编译环境可能产生的结果不同,如这个程序在Windows下的VC6.0下编译, 
int *p=(int *)(*(unsigned int *)_ebp-4-4-4-4-8-7*4);
即可得到正确的结果,而在Fedora12下的GCC编译确需
int *p=(int *)(*(unsigned int *)_ebp-4-4-4-4-8-7*4-8);才能得到正确的结果。这是因为Fedora12下的GCC编译汇编代码中多了一条
andl $-16, %esp    #使栈顶指针十六字节对齐,即其值为十六的倍数,这使得栈顶指针的值向下多移了8个字节。可通过如下方法验证:


Fedora12下的GCC编译环境下,其中int *p=(int *)(*(unsigned int *)_ebp-4-4-4-4-8-7*4);直接编译运行得不到正确结果。但是先将main.c文件通过命令gcc -S -o main.s main.c先得到汇编文件main.s,然后将其中的andl $-16, %esp注释掉并保存,然后再将main.s通过命令
gcc -o main main.s得到可执行文件main,再运行main,则可得到正确结果。

下图为分析过程中画的图,估计大家也看不清,留给自己看的啦。。。呵呵。。。


                        C语言函数传递机制分析图



以下为一篇非常好的文章,这里列出供大家参考:

关于栈

        首先必须明确一点也是非常重要的一点,栈是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中

---> 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。

---> 寄存器esp(stack pointer)可称为“ 栈指针”。

       要知道的是:

---> ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。

---> esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。

       见下图,假设函数A调用函数B,我们称A函数为"调用者",B函数为“被调用者”则函数调用过程可以这么描述:

(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。

(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。

(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。

(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。

这个过程在AT&T汇编中通过两条指令完成,即:

       leave

       ret

      这两条指令更直白点就相当于:

      mov   %ebp , %esp

      pop    %ebp

2.举个简单的实例,从汇编的视角看函数调用

2.1建立一个简单的程序,程序文件名为  main.c

    开发测试环境:

    Ubuntu 12.04

    gcc版本:4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)  (是Ubuntu自带的)

  1. "font-size:18px;">/*main.c代码:*/  
  2.   
  3. void swap(int *a,int *b)  
  4. {  
  5.    int c;  
  6.    c = *a;   
  7.    *a = *b;  
  8.    *b = c;  
  9. }  
  10.   
  11. int main(void)  
  12. {  
  13.    int a ;  
  14.    int b ;  
  15.    int ret;  
  16.    a =16;  
  17.    b = 64;  
  18.    ret = 0;  
  19.    swap(&a,&b);  
  20.    ret = a - b;  
  21.    return ret;  
  22. }  

2.2编译


#gcc    -g   -o   main   main.c

#objdump   -d  main   >   main.dump

#gcc   -Wall   -S  -o   main.s   main.c


        这样大家可以看main.s也可以看main.dump,这里我们选择使用main.dump。

        截取关键的部分,即_start,   swap  ,  main,为什么会有_start呢,因为ELF格式的入口其实是_start而不是main()。下面的图展示了main()函数调用swap()前后的栈空间的结构。右边的数字代表相对帧指针的偏移字节数。后面我们使用GDB调试就会发现栈的变化跟下图是一致的。

(!!!请注意,由于栈对齐的缘故,编译器分配栈空间时可能会有没用到的内存地址,而这些没使用到的内存地址就没在下图表示出来,所以下图只能当作示意图来了解函数栈帧结构!!具体的栈内存内容以下文的GDB调试的信息为准!!!)

      下面是main.dump中_start的代码注释,比较重要的是对esp的栈对齐操作,esp是16字节对齐的,注意左边行号的右边的0x8048300一类的数字是指令地址。



      下面是main.dump中swap()函数和main()函数的汇编代码,代码旁有详细的注释。



    下面我们使用GDB调试main.c的代码,使用刚才编译好的main镜像。

# gdb    start      (启动gdb)

# (gdb) file     main      (加载镜像文件)

# (gdb) break  main     (把main()设置为断点,注意gdb并没有把断点设置在main的第一条指令,而是设置在了调整栈指针为局部变量保留空间之后)

# (gdb) run                     (运行程序)

# (gdb) stepi                  (单步执行,不熟悉gdb的童鞋要注意了,stepi命令执行之后显示出来的源代码行或者指令地址,都是即将执行的指令,而不是刚刚执行完的指令!)
















主要的调用惯例

调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右至左的顺序压参数入栈 下划线+函数名
stdcall 函数本身 从右至左的顺序压参数入栈 下划线+函数名+@+参数的字节数, 如函数 int func(int a, double b)的修饰名是 _func@12
fastcall 函数本身 头两个 DWORD(4字节)类型或者更少字节的参数 被放入寄存器,其他剩下的参数按从右至左的顺序入栈 @+函数名+@+参数的字节数
pascal 函数本身 从左至右的顺序入栈 较为复杂,参见pascal文档

 C语言默认的调用惯例是 cdecl:

  1. 参数从右至左压栈
  2. 调用完成后调用者负责恢复栈

  虽然 VC、gcc 都默认使用 cdecl 调用惯例,但它们的实现却各有风格:

  • VC 一般是从右至左 push 参数,call,add esp, XXX
  • 而 gcc 在给局部变量分配空间的时候也给参数分配了足够的空间,所以只要从右至左 mov 参数, XXX(%esp),call 就可以了,调用者根本不用去恢复栈,因为传参数的时候并没有修改栈指针 esp。





















阅读(2798) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~