[汇编与C语言关系]3. 变量的存储布局

以下面C程序为例:

#include <stdio.h>

const int A = 10;
int a = 20;
static int b = 30;
int c;

int main(void)
{
    static int a = 40;
    char b[] = "Hello World";
    register int c = 50;

    printf("Hello World%d\n", c);

    return 0;
}

  我们在全局作用域和main函数的局部作用域各定义了一些变量,并且引入一些新的关键字const, static, register来修饰变量,那么这些变量的存储空间是怎么分配的呢?我们编译之后用readelf命令看它的符号表,了解各变量的地址分布。下面的清单中原作者把符号表按地址从低到高重新排列了,并且只截取了我们关心的那几行:

  

  变量A用const修饰,表示A是只读的,不可修改,它被分配的地址是0x8048540,从readelf的输出可以看到这个地址位于.rodata段:

  

  

  它在文件中的地址是0x538~0x554,我们用hexdump命令看这个段的内容:

  

  其中0x540地址处的0a 00 00 00 就是变量A,我们还看到程序中的字符串字面值"Hello world %d\n"分配在.rodata段的末尾,字符串的字面值是只读的,相当于在全局作用域定义了一个const数组:

  

  程序加载运行时,.rodata段和.text段通常合并到一个Segment中,操作系统将这个Segment只读保护起来,防止意外改写。这一点从readelf的输出也可以看出来:

  

  注意,像A这种const变量在定义时必须初始化。因为只有初始化才有机会给它一个值,一旦定义之后就不能再改写了,即不能再赋值。

  从上面readelf的输出可以看到.data段从地址0x804a010开始,长度是0x14,也就是到地址0x804a024结束。在.data段中有三个变量,a,b和a.1589。

  a是一个GLOBAL符号,而b被static关键字修饰了,导致它成为一个LOCAL的符号,所以static在这里的作用是声明b这个符号为LOCAL的,不被链接器处理,如果把多个目标文件链接在一起,LOCAL的符号只能在某一个目标文件中定义和使用,而不能定义在一个目标文件中却在另一个目标文件中使用。一个函数定义前面也可以用static修饰,表示这个函数名符号是LOCAL的。

  还有一个a.1589是什么呢?它是main函数中的static int a。函数中的static变量不同于局部变量,它并不是在调用函数时分配在函数返回时释放,而是像全局变量一样静态分配,所以用"static"这个词。另一方面,函数中的static变量的作用域和局部变量一样只在函数中起作用,比如main函数中的a这个变量名只在main函数中起作用,所以编译器给它的符号加了一个后缀以便和全局变量a以及其他函数的变量a区分开。

  .bss段从地址0x804a024开始,长度为0xc,也就是到地址0x804a030结束。变量c位于这个段。从上面的readelf输出可以看到.data和.bss在加载时合并到一个Segment中,这个Segment是可读写的。.bss段和.data段的不同之处在于.bss段在文件中不占存储空间,在加载时这个段用0填充。所以全局变量如果不初始化则初值为0,也分配在.bss段。

  现在还剩下函数中的b和c这两个变量没有分析。函数的参数和局部变量是分配在栈上的,b是数组也一样,也是分配在栈上的,我们看main函数的反汇编代码:

  

  可见,给b初始化用的这个字符串"Hello world"并没有分配在.rodata段,而是直接写在指令里了,通过三条movl指令把12个字节写到栈上,这就是b的存储空间,如下图所示:

  

  虽然栈是从高地址向低地址增长的,但数组总是从低地址向高地址排列的,按从低地址到高地址的顺序依次是b[0]、b[1]、b[2]......

  数组元素b[n]的地址 = 数组的基地址(b做右值就表示这个基地址) + n x 每个元素的字节数,当n=0时,元素b[0]就是数组的基地址,因此数组下标要从0开始而不是从1开始。变量c并没有在栈上分配存储空间,而是直接存在eax寄存器里,后面调用printf也是直接从eax寄存器里取出c的值当参数压栈,这就是register关键字的作用,指示编译器尽可能分配一个寄存器来存储这个变量。调用printf时对于"Hello world %d\n"这个参数压栈的是它在.rodata段中的首地址,而不是把整个字符串压栈。所以字符串在使用时可以看做数组名,如果做右值则表示数组首元素的地址。

  我们用全局变量和局部变量这两个概念主要是从作用域上区分的,现在看来用着两个概念给变量区分太笼统了,需要进一步细分。我们总计一下相关的C语法:

  作用域这个概念使用于所有标识符,而不仅仅是变量,C语言的作用域分为一下几类:

  • 函数作用域,标识符在整个函数中都有效。只有语句标号属于函数作用域。标号在函数中不需要先声明后使用,在前面用一个goto语句也可以跳转到后面的某个标号,但仅限于同一个函数之中。
  • 文件作用域, 标识符从它声明的位置开始知道这个程序文件的末尾都有效。例如上例中main函数外面的A、a、b、c还有main也算,printf其实是在stdio.h中声明的被包含到这个程序文件中了,所以也算文件作用域的。
  • 块作用域, 标识符位于一对{}括号中(函数体或语句块),从它声明的位置开始到右}括号之间有效。例如上例中main函数里的a、b、c,此外,函数定义中的形参也算块作用域的,从声明的位置开始到函数末尾之间有效。
  • 函数原型作用域, 标识符出现在函数原型中,这个函数原型只是一个声明而不是定义(没有函数体),那么标识符从声明的位置开始到在这个原型末尾之间有效。例如int foo(int a, int b);中的a和b。

  对属于同一命名空间的重名标识符,内层作用域会覆盖外层作用域的标识符。命名空间可分为以下几类:

  • 语句标号单独属于一个命名空间。例如在函数中局部变量和语句标号可以重名,互不影响。由于使用标号的语法和使用其它标识符的语法都不一样,编译器不会把它和别的标识符弄混。
  • struct, enum和union的类型Tag属于一个命名空间。由于Tag前面总是带struct, enum, union关键字,所以编译器不会把它和别的标识符弄混。
  • strcut和union的成员名属于一个命名空间。由于成员名总是通过.或->运算符来访问而不会单独使用,所以编译器不会把ta和别的标识符弄混。
  • 所有其他标识符,例如变量名、函数名、宏定义、typedef的类型名、enum成员等等都属于同一个命名空间,如果有重名的话,宏定义覆盖所有其他标识符,因为它在预处理阶段而不是编译阶段处理,除了宏定义之外其它几类标识符按上面所说的规则处理,内层作用域覆盖外层作用域。

  标识符的链接属性有三种:

  • 外部链接(External Linkage), 如果最终的可执行文件由多个程序文件链接而成,一个标识符在任意程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有External Linkage。具有External的标识符编译后在符号表中是GLOBAL的符号。例如上例中main函数外面的a和c,main和printf也算。
  • 内部链接(Internal Linkage),如果一个标识符在某个程序文件中即使声明多次也都代表同一个变量或函数,则这个标识符具有Internal Linkage。例如上例中main函数外面的b。具有Internal Linkage的标识符编译后在符号表中是LOCAL的符号,但main函数里面那个b不能算Internal Linkage,因为即使在同一个程序文件中,在不同的函数中声明多次,也不代表同一个变量。
  • 无链接(No Linkage) 。除以上情况之外的标识符都属于No Linkage的,例如函数的局部变量以及不表示变量和函数的其他标识符。

  存储类修饰符(Storage Class Specifier)有以下几种关键字,可以修饰变量或函数声明:

  • static ,用它修饰的变量的存储空间是静态分配的,用它修饰的文件作用域的变量或函数具有Internal Linkage。
  • auto ,用它修饰的变量在函数调用时自动在栈上分配存储空间,函数返回时自动释放,例如上例中 main 函数里的 b 其实就是用 auto 修饰的,只不过 auto 可以省略不写, auto 不能修饰文件作用域的变量。
  • register ,编译器对于用 register 修饰的变量会尽可能分配一个专门的寄存器来存储,但如果实在分配不开寄存器,编译器就把它当 auto 变量处理了, register 不能修饰文件作用域的变量。现在一般编译器的优化都做得很好了,它自己会想办法有效地利用CPU的寄存器,所以现在 register 关键字也用得比较少了。
  • extern ,上面讲过,链接属性是根据一个标识符多次声明时是不是代表同一个变量或函数来分类的, extern 关键字就用于多次声明同一个标识符,下一章再详细介绍它的用法。
  • typedef ,它并不是用来修饰变量的,而是定义一个类型名。typedef 在语法结构中出现的位置和是面几个关键字一样,也是修饰变量定义的,所以从语法(而不是语义)的角度把它和前面几个关键字归类到一起。

  上面介绍的 const 关键字不是一个Storage Class Specifier,虽然看起来它也修饰一个变量声明,但是在以后介绍的更复杂的声明中 const 在语法结构中允许出现的位置和Storage Class Specifier是不完全相同的。 const 和以后要介绍的 restrict 和 volatile 关键字属于同一类语法元素,称为类型限定符(Type Qualifier)。

  变量的生存期(Storage Duration,或者Lifetime)分为以下几类:

  • 静态生存期(Static Storage Duration),具有外部或内部链接属性,或者被 static 修饰的变量,在程序开始执行时分配和初始化一次,此后便一直存在直到程序结束。这种变量通常位于 .rodata , .data 或 .bss 段,例如上例中 main 函数外的 A , a , b , c ,以及 main 函数里的 a 。
  • 自动生存期(Automatic Storage Duration),链接属性为无链接并且没有被 static 修饰的变量,这种变量在进入块作用域时在栈上或寄存器中分配,在退出块作用域时释放。例如上例中 main 函数里的 b 和 c 。
  • 动态分配生存期(Allocated Storage Duration),以后会讲到调用 malloc 函数在进程的堆空间中分配内存,调用 free 函数可以释放这种存储空间。
时间: 2024-10-15 01:23:59

[汇编与C语言关系]3. 变量的存储布局的相关文章

[汇编与C语言关系]2. main函数与启动例程

为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题 在<x86汇编程序基础(AT&T语法)>一文中我们汇编和链接的步骤是: $ as hello.s -o hello.o $ ld hello.o -o hello 我们用gcc main.c -o main开编译一个c程序,其实际分为三个步骤:编译.汇编.链接 $ gcc -S main.c 生成汇编代码 $ gcc -c main.s 生成目标文件 $ gcc main.o 生成可执行文件 我们

[汇编与C语言关系]4. 结构体和联合体

用反汇编的方法研究一下C语言的结构体: #include <stdio.h> int main(int argc, char ** argv) { struct { char a; short b; int c; char d; } s; s.a = 1; s.b = 2; s.c = 3; s.d = 4; printf("%u\n", sizeof(s)); return 0; } main函数中几条语句的反汇编结果如下: 从访问结构体成员的指令可以看出,结构体的四个成

[汇编与C语言关系]5. volatile限定符

现在研究一下编译器优化会对生成的指令产生什么影响,在此基础上介绍C语言的volatile限定符.首先看下面的C程序: /* artificial device registers */ unsigned char recv; unsigned char send; /* memory buffer */ unsigned char buf[3]; int main(void) { buf[0] = recv; buf[1] = recv; buf[2] = recv; send = ~buf[0

[汇编与C语言关系]1.函数调用

对于以下程序: int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(void) { foo(2, 3); return 0; } 在编译时加上-g选项,用objdump反汇编时可以把C代码和汇编代码穿插起来显示: 反汇编的结果很长以下是截取要分析的部分: 整个程序的执行过程是main调用foo, foo调用bar, 用gdb跟踪程序的执行,直

从汇编来看c语言之变量

1.基础研究 对如图程序进行编译连接,再用debug加载. 我们在偏移地址1fa处查看main函数的内容: 执行到1fd处,发现n的偏移地址为01a6,段地址存储在ds寄存器里,为07c4. 再查看函数f2: 参数a.b的值是用栈来传递的,它们的段地址都存放在ss寄存器中: 局部变量c的值在这里是用si寄存器存储的,因为c正好是int型,那么子函数里定义的局部变量是用寄存器存储吗?我们在这里加一条赋值语句看看会如何: 可见,局部变量d是放在栈里的,而c是放在寄存器si里的,只是函数要将c返回,就

c语言 变量的存储类别以及对应的内存分配?

<h4><strong>1.变量的存储类别</strong></h4>从变量值存在的角度来分,可以分为静态存储方式和动态存储方式.所谓静态存储方式指在程序运行期间由系统分配固定的存储空间的方式(<strong>程序开始执行时分配,在程序完毕时释放,在程序过程中它们占据国定的存储单元,而不是动态分配和释放</strong>).而动态存储方式在运行期间根据需要进行动态存储方式(<strong>在程序过程中申请和释放的一些空间&

C语言进阶_变量属性

人们总说时间会改变一些,但实际上这一切还得你自己来. 一.概念详解 变量:计算机语言中储存计算结果,其值可以被修改.通过变量名来访问计算机中一段连续的内存空间. 属性:区别于同类事物的特征. C语言中变量的属性关键字有:auto register static extern 二.属性关键字详解 ①auto 用于修饰局部变量的默认属性修饰关键字,表明将自动变量表明存储在栈中. @note:auto只能修饰局部变量,修饰全局变量会报错. 1 #include <stdio.h> 2 auto in

C语言的指针变量

C语言的指针变量 在C语言中,变量是固定范围的存储空间,它存储的是赋给他的值, 比如: int a = 12; /* 这里是定义一个整型变量a,并把12这个值存储在a的地址空间上 这个地址空间是系统随机分配的,对用户是透明的不用关心 */ 指针变量存储的是变量的地址, 比如: int a = 12 , *b; b = &a; /*如上,定义了一个整型变量a并赋值12,一个整型指针变量b 然后 用 & 取值运算符 取到变量a的地址空间值,存储到指针变量b中 此时变量b中存储的是变量a中的地址

C语言常量与变量

对于基本数据类型量,按其值是否可变又分为常量和变量两种. 在程序执行过程中,其值不发生改变的量称为常量,其值可变的量称为变量.它们可与数据类型结合起来分类,例如,可分为整型常量.整型变量.浮点常量.浮点变量.字符常量.字符变量. 常量 在程序执行过程中,其值不发生改变的量称为常量.常量分类: 常量 说明 直接常量(字面量) 可以立即拿来用,无需任何说明的量,例如: 整型常量:12.0.-3: 实型常量:4.6.-1.23: 字符常量:‘a’.‘b’. 符号常量 用标识符代表一个常量.在C语言中,