春暖花开

C 语言中变量的段

前言

在 C 语言中,不同的变量位于不同的段,下面进行简单分析。

看下面一个简单的 C 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
char *myname="Bao Yungang";
char gdata[128];
char bdata[16] = {1,2,3,4};
main() {
char * ldata[16];
char * ddata;
ddata = malloc(16);
printf("myname:%llX\n", myname);
printf("main: %llX\n", main);
printf("gdata: %llX\nbdata:%llX\nldata:%llx\n&ddata:%llx\nddata: %llx\n",
gdata,bdata,ldata,&ddata,ddata);
free(ddata);
return 1;
}

我们分析一下程序中出现的变量所位于的段。

分析

首先,根据所学知识进行分析:

  • myname 变量是一个全局变量,并且进行了初始化,指向一个字符串常量。因此,myname 变量本身位于数据段:.data
  • myname指向的字符串是一个字符串常量,根据 C 语言知识,是只读的,所以应位于只读数据段:.rodata
  • gdata是一个为进行初始化的全局变量,因此位于BSS段(Block Started by Symbol)
  • bdatamyname一样是初始化了的全局变量,因此位于数据段。
  • ldata是一个main函数中的局部变量,因此位于栈中。
  • ddata变量自身也是main函数中的一个局部变量,因此也位于栈中。
  • ddata指向的内存空间是通过malloc函数动态分配的,因此位于堆中。

验证

下面,通过objdump来进行验证:

首先,使用gcc来编译源程序(Ubuntu 16.04.4 + gcc 5.4.0):

1
gcc -save-temps addr_space.c

通过添加 -save-temps选项来生成中间文件。

下面,使用 objdump 来查看段信息:

1
objdump -D ./a.out

通过-D选项显示出所有段的内容。

  • 首先看bdatamyname ,我们可以直接在 .data 段看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Disassembly of section .data:
0000000000601040 <__data_start>:
...
0000000000601048 <__dso_handle>:
...
0000000000601050 <myname>:
601050: 68 07 40 00 00 pushq $0x4007
...
0000000000601060 <bdata>:
601060: 01 02 add %eax,(%rdx)
601062: 03 04 00 add (%rax,%rax,1),%eax
...
  • 对于 gdata ,也可以直接在 .bss 段看到:
1
2
3
4
5
6
7
Disassembly of section .bss:
0000000000601080 <completed.7585>:
...
00000000006010a0 <gdata>:
...
  • 下面看myname 所指向的字符串常量。

首先,运行程序,输出如下:

1
2
3
4
5
6
myname:400768
main: 400626
gdata: 6010A0
bdata:601060
ldata:7ffc84068960
ddata:1490010

由运行结果知,myname的值为 400768,它即为 myname所指向的字符串常量的地址。然后我们在 objdump的输出内容中搜索该地址,得到它位于 .rodata中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Disassembly of section .rodata:
0000000000400760 <_IO_stdin_used>:
400760: 01 00 add %eax,(%rax)
400762: 02 00 add (%rax),%al
400764: 00 00 add %al,(%rax)
400766: 00 00 add %al,(%rax)
400768: 42 61 rex.X (bad)
40076a: 6f outsl %ds:(%rsi),(%dx)
40076b: 20 59 75 and %bl,0x75(%rcx)
40076e: 6e outsb %ds:(%rsi),(%dx)
40076f: 67 61 addr32 (bad)
400771: 6e outsb %ds:(%rsi),(%dx)
400772: 67 00 6d 79 add %ch,0x79(%ebp)
400776: 6e outsb %ds:(%rsi),(%dx)
400777: 61 (bad)
400778: 6d insl (%dx),%es:(%rdi)
400779: 65 3a 25 6c 6c 58 0a cmp %gs:0xa586c6c(%rip),%ah # a9873ec <_end+0xa3862cc>
400780: 00 6d 61 add %ch,0x61(%rbp)
400783: 69 6e 3a 20 25 6c 6c imul $0x6c6c2520,0x3a(%rsi),%ebp
40078a: 58 pop %rax
40078b: 0a 00 or (%rax),%al
40078d: 00 00 add %al,(%rax)
40078f: 00 67 64 add %ah,0x64(%rdi)
400792: 61 (bad)
400793: 74 61 je 4007f6 <__GNU_EH_FRAME_HDR+0x36>
400795: 3a 20 cmp (%rax),%ah
400797: 25 6c 6c 58 0a and $0xa586c6c,%eax
40079c: 62 (bad)
40079d: 64 61 fs (bad)
40079f: 74 61 je 400802 <__GNU_EH_FRAME_HDR+0x42>
4007a1: 3a 25 6c 6c 58 0a cmp 0xa586c6c(%rip),%ah # a987413 <_end+0xa3862f3>
4007a7: 6c insb (%dx),%es:(%rdi)
4007a8: 64 61 fs (bad)
4007aa: 74 61 je 40080d <__GNU_EH_FRAME_HDR+0x4d>
4007ac: 3a 25 6c 6c 78 0a cmp 0xa786c6c(%rip),%ah # ab8741e <_end+0xa5862fe>
4007b2: 64 64 61 fs fs (bad)
4007b5: 74 61 je 400818 <__GNU_EH_FRAME_HDR+0x58>
4007b7: 3a 25 6c 6c 78 0a cmp 0xa786c6c(%rip),%ah # ab87429 <_end+0xa586309>
...

因此,说明 myname指向的字符串常量位于 .rodata 中。事实上,看中间一栏,从 400768 开始,到 400772 的第一个数结束,其中的内容 42 61 6f 20 59 75 6e 67 61 6e 67对应的 ASCII 码就是字符串 “Bao Yungang” 。

  • 对于 ldataddata 以及 ddata 所指向的分配空间,很难直接从 objdump 的输出中看出来。我们再把 ddata变量本身的地址也打印出来:
1
2
3
4
5
6
7
myname:400768
main: 400626
gdata: 6010A0
bdata:601060
ldata:7ffe92607930
&ddata:7ffe92607928
ddata: 221a010

我们看到,ldataddata的地址非常大,而 ddata所指向的分配空间的地址相对来说要小很多,这其实也一定程度上验证了我们的结论,根据我们已知的结论,在程序执行过程中,函数中的局部变量是在函数压栈以后,在栈中分配的,而 malloc 函数分配的空间则是在堆中进行分配的,而栈是从上往下长(高地址到低地址),堆则相反,是从下往上长(低地址到高地址),因此,栈中的变量地址较大,而堆中的则较小,所以,程序运行结果与之相符。

0%