小郭学C语言(一)

本文通过分析C语言在VC6Debug环境下函数调用的堆栈操作,探讨了如何在test函数中影响main函数的变量m。通过汇编代码,解释了堆栈分配、局部变量的存储以及函数调用时的堆栈变化,展示了如何找到变量m相对于变量t的地址偏移。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

问题:

如何在下面的test函数里加入代码可以使程序运行起来输入和输出的相等?

  

(环境是vc6Debug方式下)

#include<stdio.h>

void test()

{

  int t;

  scanf("%d",&t);

  在这里加入代码

 }

main()

{

  int m;

  test();

  printf("m=%d",m);

}

 

要在test函数中加入代码,影响main函数里面的变量,很明显我们要加的代码是一个赋值语句,语句的右值是我们输入的t的值,而左值,则是test函数里t变量的地址加上main函数里m变量的地址相对于t变量的偏移的值。

 

那么问题的实质,就转变为变量m的地址相对于变量t地址的偏移了。

这个偏移如何求?

 

基础知识:

让我们跳出这个题,看看我们手头掌握的知识。

函数调用需要用到堆栈,当程序调用函数时,C将函数调用后面的指令地址(称为返回地址)压入堆栈。然后,C将函数的参数从右至左依次压入堆栈。最后,如果函数声明了局部变量,C将堆栈空间分配给函数以存储变量的值。

当函数结束的时候,C释放存储局部变量和参数的堆栈空间。然后,C根据返回地址判断下一步要执行的指令。C从堆栈中移走返回值并将地址放入IP寄存器中。

函数调用的堆栈示意图如下图所示:

 

 

 

 

 

 

 

 

 

 

需要说明的是,什么是堆栈呢?我们都知道,变量、常量在内存中的分配大致分成四个区域,分别是堆栈、堆、常数存储区以及全局、静态存储区。分配在堆栈中的变量具有的最主要的特点就是当这个变量不再需要的时候,编译器会自动回收,函数内的局部变量的存储单元都可以在栈上分配。

实际上,堆栈是系统内存中的一块区域,这个区域在操作系统初始化时得到分配。

 

探究本题:

由堆栈的知识,我们知道了,test函数里的t,以及main函数里的m都是分配在堆栈中的,因此他们之间的地址偏移量,也就取决于函数调用时堆栈是如何操作的。

我们通过汇编代码来仔细分析下堆栈的操作方式,调试坏境是VC6.0,在windows操作系统中,堆栈的生长方向是由高到低的。

 

32:

33:

34:   main()

35:   {

00401070   push        ebp

00401071   mov         ebp,esp                                     

00401073   sub         esp,44h                        

00401076   push        ebx

00401077   push        esi

00401078   push        edi

00401079   lea         edi,[ebp-44h]                    

0040107C   mov         ecx,11h

00401081   mov         eax,0CCCCCCCCh

00401086   rep stos    dword ptr [edi]                    

36:     int m;

37:     test();

00401088   call        @ILT+0(test) (00401005)           

38:     printf("m=%d",m);

0040108D   mov        eax,dword ptr [ebp-4]

00401090   push        eax

00401091   push        offset string "m=%d" (0042201c)

00401096   call         printf (00401160)

0040109B   add         esp,8                           

39:   }

0040109E   pop         edi

0040109F   pop         esi

004010A0   pop         ebx

004010A1   add         esp,44h

004010A4   cmp         ebp,esp

004010A6   call        __chkesp (00401120)

004010AB   mov         esp,ebp

004010AD   pop         ebp

004010AE   ret                                          

--- No source file  ----

 

下图是到步骤⑤时的堆栈情况,实际上函数main()test()一样,也可以看做是函数调用的情况,只不过函数main是由mainCRTStartup这个默认函数调用的,而函数test是由函数main调用的,两者在调用上的汇编代码都是一样的。

 

好了,堆栈的全貌我们看到了,现在就要真正的开工了。我们结合汇编代码来看看到底函数调用时,堆栈是如何操作的。

当一切都还没有发生却即将发生的时候,是一个令人鸡冻的时刻,是时候该向过去告别了,因为我们即将进入一个新的函数,一段新的旅程。但是别急,磨刀不误砍柴功,有些事情必须先做准备,那么就从这里开始准备吧。

此时,栈顶esp的值是0x12ff84,这个地址上存的值是0x4012c9,我们再去看看0x4012c9位置的指令。很好,在这个指令上一行我们看到了一条醒目的指令:call  @ILT+5(main),一切都明白了,0x4012c9就是函数的返回地址。旅行虽然令人兴奋,但是我们必须把出口的地址记下来,毕竟,旅行只是生活的调节,我们终将还得继续自己的生活。

 

     push  ebp

mov   ebp, esp

这段代码很容易,就是把栈底寄存器ebp入栈。ebp主要用于给出堆栈中数据区基址的偏移,从而方便的实现直接存取堆栈中的数据。

mov ebp, esp 就是把当前esp的值赋给ebp实际上,就是给ebp重新赋了值,该值就是这个main()函数在堆栈中的基址,以后main()函数中分配的局部变量等的地址都会通过ebp算出偏移,来进行读写运算。

我们看到0x12ff80地址上存的是0x12ffc0,这是调用main()函数的函数在堆栈中的基址,我们在这里把这个值保存下来,以便退出main()函数时能够恢复ebp

 

sub   esp, 44h

在存储了ebp,并且重新对ebp赋值了后,代码将esp减去44h,这个区域是预留给函数的局部变量的。在该函数中定义的局部变量,将依次序在这块区域中得到分配。

有人说了,万一这个44h的区域不够怎么办,实际上这个44h是可以变化的,编译器会根据实际情况调整这个数值。我测试了一下,如果定义了1个变量,就会分配44h的空间,定义了2个变量,就会分配48h的空间……依次类推。

 

push   ebx

       push   esi

push   edi

lea    edi,[ebp-44h]

分配了44h空间后,ebx, esi, edi的值将依次入栈。为什么要先分配44h的空间,再push这三个寄存器,而不是一口气把ebp, ebx, esi, edi这四个东东全部都压进去,再分配局部变量的空间呢。

我想是因为这个空间是分配给局部变量的,而所有局部变量的地址都是通过ebp加上偏移量算出来的,因此让这个空间和ebp紧挨能够提高运算的效率。

ebx寄存器是基地址寄存器,是四个数据寄存器中唯一可作为存储器指针使用的寄存器。esi寄存器是源变址寄存器,edi则是目的变址寄存器。这两个寄存器的典型用法就是进行字符串操作时,esi作为源指针,edi作为目的指针。

lea  edi, [ebp-44h] 这一句使得edi的值由0x00000000变成了(0x12ff80 – 44h = )0x12ff3c,这个地址是44h的最后一个地址,至于为什么要赋成这个值,我们马上就会知道了。

 

  mov      ecx,11h

mov      eax,0CCCCCCCCh

rep stos    dword ptr [edi] 

我们在②中分配的空间似乎还没有初始化,现在我们就着手来对这段区域进行初始化。在③中已经把edi的值赋给了这块空间的一端,而ecx这个计数寄存器里存储的是循环次数,一共要循环11h次,哦,这个11h就是44h/4得到的。很显然,eax里存储的是初值罗,这段话的意思就是,从0x12ff3c这个地址开始,以后的11h个字的值初始化为0xcccccccc

程序走到这里,基本上函数调用的步骤已经全部完成了,我们总结一下,看看堆栈里到底发生了什么。

压入了4个寄存器的值,分别是ebpebxesiedi的值。分配了一个大小为44h的空间,这个空间的大小根据函数的具体情况,由编译器自行决定。对这个大小为44h的空间的每一个字初始化为0xcccccccc

OK,我们来算算到目前为止,我们用了堆栈多少空间:0x12ff84 – 0x12ff30 = 54h

 

  call     @ILT+0(test) (00401005)

做完函数调用的工作,就要开始真正的函数本题的工作了。我们看到int m;这一句C语言并没有转化成任何的汇编语言,实际上,这只是一个声明的语句,告诉编译器这里有一个变量,叫做m。那么,什么时候分配这个m的空间呢,我们继续往下看,就会看到一句mov        eax,dword ptr [ebp-4],对头,就在这里,地址是ebp-4,正是44h的最开始的地址。

test()这句话汇编为call     @ILT+0(test) (00401005),那么我们又要开始一段新的旅程了。

 

OK, 我们现在来到了test函数,也离我们需要解答的地方越来越近了。其实我们走到了这里,聪明的你,是不是已经知道了答案了呢?

还是和之前一样,我们从汇编看起。

 

2:    void test()

3:    {

0040DA10   push        ebp                          

0040DA11   mov         ebp,esp                      

0040DA13   sub         esp,44h                      

0040DA16   push        ebx

0040DA17   push        esi

0040DA18   push        edi

0040DA19   lea         edi,[ebp-44h]                  

0040DA1C   mov         ecx,11h

0040DA21   mov         eax,0CCCCCCCCh

0040DA26   rep stos    dword ptr [edi]                   

4:      int t;

5:      scanf("%d",&t);

0040DA28   lea         eax,[ebp-4]

0040DA2B   push        eax

0040DA2C   push        offset string "%d" (00422fd8)

0040DA31   call        scanf (004010c0)

0040DA36   add         esp,8

              

7:     }                                              

0040DA3F   pop         edi

0040DA40   pop         esi

0040DA41   pop         ebx

0040DA42   add         esp,44h

0040DA45   cmp         ebp,esp

0040DA47   call        __chkesp (00401120)

0040DA4C   mov         esp,ebp

0040DA4E   pop         ebp

0040DA4F   ret

--- No source file  ---

 

堆栈如下图所示:

 

 

 

 

 

 

 

函数的调用和main()函数一样,而如前所述,局部变量t分配到44h空间的第一个字,也就是0x12ff24这个地址。而局部变量m则分配到main()函数44h空间的第一个字,也就是0x12ff7c中。

OK,我们至此找到了变量t和变量m的偏移,0x12ff7c – 0x12ff24 = 58h,因此我们问题的答案就显而易见了:

*((&t) + 0x58 / 4) = t;

 

发散思维:

题目已经解决了,但是我们注意到了,test()函数没有参数,显然,我们也很想知道,如果这个函数有参数,堆栈将如何工作。

那么,把题目里的程序改一下子吧:

 

#include<stdio.h>

void test(int x, char *pStr, double y)

{

  int t;

  scanf("%d",&t);

  *((&t) + 0x58/4) = t;

 }

 

void main()

{

  int m;

  test(1, "Hello World!", 3.14);

  printf("m=%d",m);

}

test()函数加上了3个参数,分别是intchar *以及double型的,编译环境依然是Windows操作系统,VC++6.0

 

我们来看看关键部分的汇编代码:

14:     test(1, "Hello World!", 3.14);

00401088   push        40091EB8h                         

0040108D   push        51EB851Fh                          

00401092   push        offset string "Hello World!" (00426028)    

00401097   push        1                                   

00401099   call        @ILT+0(test) (00401005)                

0040109E   add         esp,10h                              

    涉及到这一段代码的堆栈如下图:

 

 

 

 

 

 

 

 

 

 

 

 

①②③分别把三个参数压入栈中,顺序是从右向左。

先压入函数的参数,再压入返回地址,与基础知识里讲的正好相反,可见压入的顺序是与编译器有关的。

 

总结:

windows操作系统,VC Debug环境下:

1,  函数的局部变量是分配在栈中的,这个区域是将ebp压入栈后分配的一块区域。这也是为什么分配在栈上的局部变量在函数结束后会自动收回的原因,因为函数结束后会退栈,因此分配给该函数局部变量的那块区域也就退栈掉了,自然也就自动收回了。

2,  函数调用时,会自动将ebp, ebx, esiedi压入栈,函数的局部变量是通过ebp加上偏移来进行访问的。

3,  函数的调用是先将参数由右向左压入栈,再压入下一条指令的地址作为返回地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值