免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 4245 | 回复: 11
打印 上一主题 下一主题

[函数] 请教关于c和汇编函数调用的几个问题 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2007-09-12 19:50 |只看该作者 |倒序浏览
大家好,在看书的时候有点东西不太懂,所以来请教。

代码如下:
void foo(void)
{
        int i=1;
}

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


反汇编以后不太懂

1,被调用函数中,要先用sub 为局部变量预留空间,而不是用push直接压入栈中,如果i初始化为一个数值的话也是先留好空间再用mov指令放入栈中,而不是push,这主要是出于什么目的?还有这种行为是由编译器决定还是由平台决定的?
  还有为局数变量在栈中预留的字节数,我只声明了一个int,可是却分配了0x10,这个数是怎么来的?

2,被调用函数返回时原书提到:
到达函数的结尾并且esp寄存器被设置回其原始值时,局部变量会从堆栈丢失,并且无法从发出调用的程序使用esp或者ebp寄存器直接访问它们(这就是“局部变量”这个术语的由来)

我按照书上的例程试验了一下,即使被调用函数退出以后,如果栈中的内容没有被覆盖,就还是可以通过esp指针访问啊,当然我是在gdb里面测试的


最后还有一个小问题,我们在任何情况下都不可能通过mov指令改变eip的值?

谢谢!

[ 本帖最后由 郭德纲 于 2007-9-12 19:53 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2007-09-12 21:33 |只看该作者
1、用sub是为变量预留空间,这和push参数是两码事
2、书上说的是另一层意思。
3、是的。

论坛徽章:
0
3 [报告]
发表于 2007-09-12 23:25 |只看该作者

回复 #2 mik 的帖子

谢谢楼上的回复。

第一个问题,我可能没有表达清楚,我重新说一下。
假设函数A调用函数B:A-->B.
我对A、B两个函数之间参数传递没有疑问,我不懂的是B的局部变量在栈中的分配。
按我的理解,反正声明局部变量都是在函数的开头部分,等执行到B的时候把这些变量一个一个全部用push入栈,访问的时候用ebp加偏移即可。
可实际上却是用sub给所有变量预留空间,等给它们赋值的时候再按(赋值的)顺序放入栈中,相比push的简单,后者的行为让我迷惑。所以才有“此行为是编译器决定还是平台决定”的问题。

写完这些东西又写了几个小段比较了一下,可能这些也不是什么值得深究的地方,只不过想到这里了所以提个小小疑问罢了。

第二个问题,“无法从发出调用的程序使用esp或者ebp寄存器直接访问它们”

我主要是对这句话有疑问,因为退出被调用函数以后明明还可以访问到它的局部变量啊,不太明白作者的另一层意思是什么。mik兄指点一二??

论坛徽章:
0
4 [报告]
发表于 2007-09-12 23:30 |只看该作者
>>等执行到B的时候把这些变量一个一个全部用push入栈,访问的时候用ebp加偏移即可。
什么意思? 你是说在A中把B中的变量一个个push栈?? A 怎么会知道B有什么变量呀???

>> “无法从发出调用的程序使用esp或者ebp寄存器直接访问它们”
确实无法。这个问题和第1个问题同样的道理

论坛徽章:
0
5 [报告]
发表于 2007-09-13 00:01 |只看该作者
原帖由 郭德纲 于 2007-9-12 23:25 发表
谢谢楼上的回复。

第一个问题,我可能没有表达清楚,我重新说一下。
假设函数A调用函数B:A-->B.
我对A、B两个函数之间参数传递没有疑问,我不懂的是B的局部变量在栈中的分配。
按我的理解,反正声明局部 ...

>>可实际上却是用sub给所有变量预留空间,等给它们赋值的时候再按(赋值的)顺序放入栈中,相比push的简单,后者的行为让我迷惑。所以才有“此行为是编译器决定还是平台决定”的问题。
lz可能想混淆了。程序在编译后只是一堆二进制代码,在没运行前,哪儿来栈啊。所以,如何做到先把变量push到栈上呢?
所以,一个函数在执行时,按变量顺序一个个的push到栈上,用的时候再按相反的顺序一个个的pop出来,那么如何灵活的访问任意一个变量呢?


>>无法从发出调用的程序使用esp或者ebp寄存器直接访问它们”
是可以的。一个函数退出后,栈指针的值变了,这个时候栈缩小了。但OS通常不会马上释放栈缩小前那部分栈所对应的内存的,只要在OS释放前访问即可。例如下面这个例子:

  1. #include <stdio.h>

  2. int h()
  3. {
  4. char a[] = {[0 ... 100] = 10};

  5. return a[10];

  6. }
  7. int main()
  8. {
  9.     char *p = NULL;
  10.     h();
  11.     p = (char *)((int)&p - 60);

  12.     printf("%d\n",*p);
  13. }

复制代码


>>最后还有一个小问题,我们在任何情况下都不可能通过mov指令改变eip的值?
也是可以的,只是应用程序不能这样做。
调试器,例如gdb,就是通过ptrace系统调用修改进程eip值来插入代码的

论坛徽章:
0
6 [报告]
发表于 2007-09-13 00:32 |只看该作者
第一个和第三个问题我再想想啊,还有是点迷惑,顺便想想怎么完整的表达我的想法。


先说第二个,我是这样做的:

[neil@localhost mytest]$ cat call.s

        .section .text
output:
        .asciz "The output is %d \n"

        .globl _start
_start:
        nop
        call func
        nop
        pushl -12(%esp)
        pushl $output
        call printf
        movl $1, %eax
        int $0x80
func:
        pushl %ebp
        movl %esp, %ebp
        pushl $3
        pushl $output
        call printf
        movl %ebp, %esp
        popl %ebp
        ret

[neil@localhost mytest]$ as call.s -o call.o
[neil@localhost mytest]$ ld call.o -o call -lc -dynamic-linker /lib/ld-linux.so.2
[neil@localhost mytest]$ ./call
The output is 3
The output is 3
[neil@localhost mytest]$


看上去和楼上朋友的结论是一样的

论坛徽章:
0
7 [报告]
发表于 2007-09-13 02:58 |只看该作者
关于第一个问题,现在有点新的认识

  1. [neil@localhost tmp]$ cat main.c

  2. void foo(void)
  3. {
  4.         int a,b,c,d;
  5.         b=2;
  6.         a=1;
  7.         c=3;
  8.         d=4;
  9. }

  10. int main()
  11. {
  12.         foo();
  13.         return 0;
  14. }
  15. [neil@localhost tmp]$ gcc -S main.c
  16. [neil@localhost tmp]$ cat main.s
  17.         .file   "main.c"
  18.         .text
  19. .globl foo
  20.         .type   foo, @function
  21. foo:
  22.         pushl   %ebp
  23.         movl    %esp, %ebp
  24.         subl    $16, %esp
  25.         movl    $2, -12(%ebp)
  26.         movl    $1, -16(%ebp)
  27.         movl    $3, -8(%ebp)
  28.         movl    $4, -4(%ebp)
  29.         leave
  30.         ret
  31.         .size   foo, .-foo
  32. .globl main
  33.         .type   main, @function
  34. main:
  35.         leal    4(%esp), %ecx
  36.         andl    $-16, %esp
  37.         pushl   -4(%ecx)
  38.         pushl   %ebp
  39.         movl    %esp, %ebp
  40.         pushl   %ecx
  41.         call    foo
  42.         movl    $0, %eax
  43.         popl    %ecx
  44.         popl    %ebp
  45.         leal    -4(%ecx), %esp
  46.         ret
  47.         .size   main, .-main
  48.         .ident  "GCC: (GNU) 4.1.2 20070502 (Red Hat 4.1.2-12)"
  49.         .section        .note.GNU-stack,"",@progbits
  50. [neil@localhost tmp]$
复制代码



从这里可以看出这么几点:
1、我原来的错误在于,认为哪个变量被赋值,则该变量入栈,现在看来,只要变量被声明,它在栈中的位置也就确定了,这可以从上面的汇编看出来。
3、变量的顺序问题,变量声明的顺序为a,b,c,d,但在栈中存储的顺序(由高地址到低地址)为d,c,b,a,我们能不能这样理解,当把esp减去某个值从而在栈中划分出一块空间后,虽然这块空间实际是在栈上,但分配变量时却把他当作堆(数据段?)来对待,从而按变量声明的顺序从低地址开始分配。(纯属猜测,因为若按push那种入栈方式的话,这四个变量在其中的存储方式不应该是这样)
2、变量的访问,访问栈中的变量是通过类似于-4(%ebp),-8(%ebp)这样的基址+偏移(不知道这个说法是否准确,瞎说的)进行的,而不是通过pop,也就是说,即使变量是采用push这种入栈方式的话
,我们也可以利用ebp来很方便的访问所有变量。
______________________________________________
当然,以上所有东西可能都是平台、编译器或者os相关的。这也是我发这帖子的原因,因为实在搞不清哪些问题是平台决定的,哪些问题是编译器决定的,还有哪些问题是os决定的。

论坛徽章:
0
8 [报告]
发表于 2007-09-13 10:26 |只看该作者
原帖由 郭德纲 于 2007-9-13 02:58 发表
关于第一个问题,现在有点新的认识

[neil@localhost tmp]$ cat main.c

void foo(void)
{
        int a,b,c,d;
        b=2;
        a=1;
        c=3;
        d=4;
}

int main()
{
       ...

  1. foo:
  2.         pushl   %ebp
  3.         movl    %esp, %ebp
  4.         subl    $16, %esp
  5.         movl    $2, -12(%ebp)
  6.         movl    $1, -16(%ebp)
  7.         movl    $3, -8(%ebp)
  8.         movl    $4, -4(%ebp)
  9.         leave
  10.         ret
复制代码

我对这段代码极度疑惑,只有在big-endian的机器上才会是以d,c,b,a的顺序排列的。但x86没有big-endian的cpu,而且这里栈也是向下增长的,按理说不应该这样排列。
我在我机器上也做了实验,结果和lz正好相反

论坛徽章:
0
9 [报告]
发表于 2007-09-13 13:15 |只看该作者
我的环境是这样:

  1. [neil@localhost tmp]$ uname -a
  2. Linux localhost.localdomain 2.6.22.4-65.fc7 #1 SMP Tue Aug 21 22:36:56 EDT 2007 i686 athlon i386 GNU/Linux
复制代码



  1. [neil@localhost tmp]$ cat /proc/cpuinfo
  2. processor       : 0
  3. vendor_id       : AuthenticAMD
  4. cpu family      : 15
  5. model           : 44
  6. model name      : AMD Sempron(tm) Processor 2800+
  7. stepping        : 2
  8. cpu MHz         : 1607.375
  9. cache size      : 256 KB
  10. fdiv_bug        : no
  11. hlt_bug         : no
  12. f00f_bug        : no
  13. coma_bug        : no
  14. fpu             : yes
  15. fpu_exception   : yes
  16. cpuid level     : 1
  17. wp              : yes
  18. flags           : fpu vme de pse tsc msr pae mce cx8 apic mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx mmxext fxsr_opt lm 3dnowext 3dnow up pni ts ttp tm stc
  19. bogomips        : 3215.73
  20. clflush size    : 64
复制代码


现在情况是这样,我们这样函数中声明变量

  1. unsigned int a=0x12345678;

  2. void foo()
  3. {
  4.         unsigned int c=0x12345678;
  5.         unsigned int d=0x12345678;
  6. }

  7. int main()
  8. {
  9.         unsigned int b=0x12345678;
  10.         foo();
  11.         return 0;
  12. }

复制代码


运行时布局是这样的(#为注释):

  1. #高地址
  2. ebp                 #调用foo之前ebp指向这里
  3. ecx
  4. b的值
  5. 未初始化
  6. 未初始化
  7. 未初始化
  8. 返回值
  9. 原始ebp地址                 #开始调用foo时ebp指向这里
  10. #接下来开始调用foo
  11. d的值
  12. c的值
  13. 未初始化
  14. 未初始化             #esp指向这里
  15. #低地址
复制代码


分配是从接近ebp的一端(高地址)开始的,只是在栈上的顺序和声明的顺序相反。

a则是在它该存在的地方从低地址开始分配。

[ 本帖最后由 郭德纲 于 2007-9-13 16:08 编辑 ]

论坛徽章:
0
10 [报告]
发表于 2007-09-13 23:36 |只看该作者

回复 #9 郭德纲 的帖子

是用gcc编译的吗?搞不懂了
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP