现在的位置: 首页 > 综合 > 正文

由swap看变量在内存中的分配

2013年08月05日 ⁄ 综合 ⁄ 共 4300字 ⁄ 字号 评论关闭

 

    在排序问题中,我们必不可少的会用到交换操作。针对不同的语言,处理方式有所不同:或者单独写一个swap函数,或者直接在排序函数中实现交换操作。采用swap函数大多数人肯定会遇到不能交换的问题,其中的原理大家也应该都很清楚:swap函数交换的是局部变量,话虽如此,但具体的机制其实远不是如此简单。

   要真正理解这个问题,就需要从程序的运行时刻环境来解释。一个程序在运行时内存会被划分成几部分:代码区、数据区、堆区和栈区。

    生成的目标代码的大小在编译时刻就已经固定下来了,因此编译器可以将可执行目标代码放在一个静态确定的代码区。这个区通常位于存储的低端。类似地,程序的某些数据对象的大小或者全局变量可以在编译时刻知道,它们可以被放置在另一个称为静态区的区域中,该区域可以被静态确定。放置在这个区域的数据对象进行静态分配,是因为这些对象的地址可以被编译到目标代码中。

   为了将运行时刻的空间利用率最大化,另外两个区域-----栈和堆被放在剩余地址空间的相对两端。这些区域是动态的,它们的大小会随着程序运行而改变。这两个区域根据需要向对方增长。栈区用来存放称为活动记录的数据结构,这些活动记录在函数调用过程中生成。有些数据的生命周期要比创造它的某此过程调用更长,这些数据通常被分配在堆区。局部变量在栈区中分配,和活动记录密切相关,所以我们只关注栈区。

   当一个方法被调用时,编译器会为其在栈区中分配一块空间,作为该方法的活动记录。活动记录中包括方法的形式参数,局部变量等数据以及方法结束时的返回地址。在该方法的生命周期中,其操作除了涉及全局的访问之外都在该空间中。活动记录的分配是随着程序的运行而逐渐分配和回收的:遇到方法调用即为该方法分配空间创建活动记录,方法结束则回收空间。

下面以用C语言写的冒泡排序为例来分析活动记录的构造。源代码BubbleSort.c为:

#include<stdio.h>
void swap(int*a,int* b)//正确的交换函数
{
         int temp=*a;
         *a=*b;
         *b=temp;
}
void swaps(inta,int b)//错误的交换函数
{
         int temp=a;
         a=b;
         b=temp;
}
voidBubbleSort(int a[],int length)
{
         int i,j;
         for(i=length-1;i>1;i--)
         {
                   for(j=0;j<i;j++)
                   {
                            if(a[j]>a[j+1])swap(&a[j],&a[j+1]);                    
                   }
         }
}
int main()
{
         int a[10];
         int i;
         for(i=0;i<10;i++)
         {
                   a[i]=rand()%100;
         }
         printf("---------------------------------------/n");
         for(i=0;i<10;i++)
         {
                   printf("%d ",a[i]);
         }
         printf("/n");
         BubbleSort(a,10);
         printf("---------------------------------------/n");
         for(i=0;i<10;i++)
         {
                   printf("%d ",a[i]);
         }
         printf("/n");
}

         用命令gcc –S BubbleSort.c生成源文件的部分汇编代码为:(注:此为AT&T汇编,源操作数和目的操作数与Intel汇编相反,细节请自行查阅)

         .file  "BubbleSort.c"
         .text
.globl swap
         .type         swap, @function
swap:
         pushl         %ebp                             ;%ebp保存父函数活动记录的基地址
         movl          %esp, %ebp                  ; %esp为当前函数活动记录基地址,保存在%ebp中
         subl  $16, %esp                               ;为当前函数的活动记录分配空间为16字节
         movl          8(%ebp), %eax             ;将&a存入eax
         movl          (%eax), %eax                ;将*a存入eax,我们可以看出指针操作就是间接寻址
         movl          %eax, -4(%ebp)             ;将eax的值存入temp(temp=*a)
         movl          12(%ebp), %eax            ;将&b存入eax
         movl          (%eax), %edx                ;将*b存入edx
         movl          8(%ebp), %eax              ;将&a存入eax
         movl          %edx, (%eax)                 ;将edx存入eax所指内存单元(*a=*b)
         movl          12(%ebp), %eax             ;将&b存入eax
         movl          -4(%ebp), %edx             ;将temp存入edx
         movl          %edx, (%eax)                 ;将edx存入eax所指内存单元(*b=*temp)
         leave
         ret
         .size swap, .-swap
 
.globl BubbleSort
         .type         BubbleSort, @function
BubbleSort:
         pushl         %ebp
         movl          %esp, %ebp
         subl  $24, %esp
         movl          12(%ebp), %eax             ;将length放到eax中
         subl  $1, %eax                                  ;eax=length-1
         movl          %eax, -4(%ebp)             ;i=length-1
         jmp  .L6
.L10:
         movl          $0, -8(%ebp)                  ;j=0
         jmp  .L7
.L9:
         movl          -8(%ebp), %eax            ;将j移到eax中
         sall   $2, %eax                                 ;将j扩大四倍
         addl 8(%ebp), %eax                       ;将数组的指针指向元素a[j],并将值放入eax
         movl          (%eax), %edx                ;将a[j]放到edx中
         movl          -8(%ebp), %eax             ;将j移到eax中
         addl $1, %eax                                 ;j=j+1
         sall   $2, %eax                                 ;将j扩大四倍
         addl 8(%ebp), %eax                       ;将数组指针指向下一个元素a[j+1],并将值放入eax
         movl          (%eax), %eax                ;将a[j+1]放到eax中
         cmpl          %eax, %edx                   ;比较a[j]和a[j+1]
         jle     .L8
         movl          -8(%ebp), %eax
         addl $1, %eax
         sall   $2, %eax
         movl          %eax, %edx
         addl 8(%ebp), %edx
         movl          -8(%ebp), %eax
         sall   $2, %eax
         addl 8(%ebp), %eax
         movl          %edx,4(%esp)               ;将edx (a[j])存入为swap的形式变量b分配的空间
         movl          %eax,(%esp)                 ;将eax(a[j+1])存入为swap的形式变量a分配的空间
         call   swap
.L8:
         addl $1, -8(%ebp)
.L7:
         movl          -8(%ebp), %eax
         cmpl          -4(%ebp), %eax
         jl       .L9
         subl  $1, -4(%ebp)
.L6:
         cmpl          $1, -4(%ebp)
         jg      .L10
         leave
         ret

     下面以swap函数的汇编代码来分析活动记录。在一般的机器中,栈都是由高地址向低地址变化,当执行swap时,由高地址向低地址为其形式参数和局部变量分配空间。首先是形式参数,在执行swap之前,编译器会按照输入参数的反向顺序为形式参数分配空间,我们所说的局部变量其实就是指的这个为形式参数分配的空间。函数执行分配空间,执行完毕释放空间,这就是局部变量的含义。然后再进入swap函数体,进入函数体第一步就是存函数的返回地址,这个过程由编译器隐式完成,在汇编代码中不存在。然后执行一个例程(the procedureprolog):pushl 
%ebp   movl  %esp, %ebp,这两句的作用是保存调用者的活动记录的初始地址,当函数返回时,可以正确的跳到父函数的活动记录。最后才是为函数swap的局部变量分配空间。该空间的分配应该是采用回填方式分配,并不局限于函数中声明的变量个数,而是函数用到的空间。这些空间构成了swap函数的活动记录。

     通过分析汇编代码,我们可以明白为什么基本类型做形参的swap函数不起作用,而指针的swap函数可以实现交换功能。我们在执行swap函数时,编译器会为该方法分配一部分空间,将形式参数放在这个空间中,而这个空间是和方法调用时的实参空间不同的,所以我们所有的修改就不会触及到实参。虽说swap结束后,形参的空间还存在,但是我们在高级语言级无法访问,也就无法获得swap交换之后的值。

     基本类型不行,但是为什么指针就可以呢?虽说swap保存的不是实际的值,但是保存了实际变量的指针,这就意味着我们可以通过指针修改实参。象Java这种高级语言没有指针,怎样实现两数交换,一般可以采用的策略是将要交换的数声明为类的实例变量,这样就可以进行交换了。

     至此,我们已经对swap函数的原理有了比较深刻的认识。我们不妨再深一步,对C语言的变量布局再通过实例分析一下:

#include<stdio.h>
#define N 10
int a,b[N],*p;//全局变量
int add(inta,int b)
{
         return a+b;
}
int main()
{
         int a=3;//局部变量
         a=add(a,a);
         printf("a=%d/n",a);
}

汇编代码为:

         .file  "test.c"
         .comm      a,4,4                                 ;全局变量会单独存放,放在静态区
         .comm      b,40,32
         .comm      p,4,4
         .text
.globl add
         .type         add,@function
add:
         pushl         %ebp
         movl          %esp,%ebp
         movl          12(%ebp),%eax
         movl          8(%ebp),%edx
         leal   (%edx,%eax),%eax                 ;AT&T汇编,实现相加功能,将返回值放在eax中
         popl %ebp
         ret
         .size add,.-add
         .section    .rodata
.LC0:
         .string      "a=%d/n"                            ;字符串也会特殊处理
         .text
.globl main
         .type         main,@function
main:
         pushl         %ebp
         movl          %esp,%ebp
         andl $-16,%esp
         subl  $32,%esp
         movl          $3,28(%esp)             ;局部变量a放在28(%esp),属于main的活动记录
         movl          28(%esp),%eax
         movl          %eax,4(%esp)
         movl          28(%esp),%eax
         movl          %eax,(%esp)
         call   add
         movl          %eax,28(%esp)        ;从eax中读取函数的返回值,返回值不单独分配空间
         movl          $.LC0,%eax
         movl          28(%esp),%edx
         movl          %edx,4(%esp)
         movl          %eax,(%esp)
         call   printf
         leave
         ret

     我们由swap函数遇到的问题入手,来分析C语言变量的分配问题,其他高级语言也有类似的特点。希望通过该文章,大家对底层变量的分配和高级语言之间的关系能更加了解。

 

抱歉!评论已关闭.