Linux c 地址空间 堆栈 数据段 代码段 变量存储位置

Linux 环境中,虚拟地址空间即用户程序可以看到的地址空间分为以下几个段,从上到下依次是栈,堆,bss,data,text

以下内容只适用于 32 位系统,64 位系统略微不同。

1、栈

1.1 栈的作用

在 c/c++ 中函数调用很常见,那么在底层函数调用是怎么实现的呢?c/c++ 中的函数调用,在会汇编指令中通过call 指令实现,当执行到 call 指令的时候,① 将函数所需参数从右到左依次入栈,例如调用 printf("a+b=%d\n",sum);,sum 入栈,字符串"a+b=%d\n"的地址入栈,② CPU 首先将call 指令之后指令地址入栈,当函数 return 的时候可以继续往下执行。当进入函数内部的时候,首先执行:

push   %ebp
mov    %esp,%ebp

此时 ebp 代表该函数的栈顶地址,一个栈帧表示一个函数的调用过程,此时在该函数内部可以自由使用 ebp 以下的内存空间,可以通过将 sp 寄存器减去特定的值,来为函数的局部变量预留空间,变量地址可以通过 bp 指针获取。如下图所示:

当函数返回的时候,要确保栈的内容和调用之前一模一样。

1.2 栈的工作过程

现在通过代码来观察栈的工作过程。以下代码定义了一个求和函数,在 main 函数中将m,n,x,y 的和 保存在sum 变量中。 其中 sum 是未初始化变量,这主要为了测试,记得不要使用未初始化的变量。

int getSum(int a,int b,int c,int d){
    return a+b+c+d;
}
int main(){
    int m = 8;
    int n = 9;
    int x = 10;
    int y = 11;
    int sum;
    sum = getSum(m,n,x,y);
    return 0;
}

gcc 使用 -g 添加调试选项,-m32 编译32位应用程序:使用 objdump 得到反汇编得到机器代码:

$ gcc -m32 -g dis.c -o dis
$ objdump -d dis

main 函数的机器码如下:

0000050a 
: 50a: 55 push %ebp 50b: 89 e5 mov %esp,%ebp 50d: 83 ec 20 sub $0x20,%esp 510: e8 3f 00 00 00 call 554 <__x86.get_pc_thunk.ax> 515: 05 c7 1a 00 00 add $0x1ac7,%eax 51a: c7 45 ec 08 00 00 00 movl $0x8,-0x14(%ebp) 521: c7 45 f0 09 00 00 00 movl $0x9,-0x10(%ebp) 528: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%ebp) 52f: c7 45 f8 0b 00 00 00 movl $0xb,-0x8(%ebp) 536: ff 75 f8 pushl -0x8(%ebp) 539: ff 75 f4 pushl -0xc(%ebp) 53c: ff 75 f0 pushl -0x10(%ebp) 53f: ff 75 ec pushl -0x14(%ebp) 542: e8 a2 ff ff ff call 4e9 547: 83 c4 10 add $0x10,%esp 54a: 89 45 fc mov %eax,-0x4(%ebp) 54d: b8 00 00 00 00 mov $0x0,%eax 552: c9 leave 553: c3 ret

sub $0x20,%esp,将 esp 减去 32,代表当前栈帧中为局部变量预留的空间为 32 字节,为什么是 32 字节?我们总共定义了 5 个int 型变量,每个变量占 4 个字节,总共 20 字节。其实为了为了方便计算,esp 每次都减去 16 的整数倍,当局部变量所占空间小于等于 16 时,就预留16字节,即减去 0x10;当大于 16,小于等于 32时,预留 32 字节,依次类推。

ebp 表示当前函数栈帧地址,通过将 ebp 减去一定的偏移量就可以定位到这些局部变量。ebp-0x4 表示 sum,ebp-0x8 表示 y,ebp-0xc 表示 x,ebp-0x10 表示 n,ebp-0x14 表示 m。

在调用 getSum 之前,y,x,n,m 依次压入堆栈,之后把 add $0x10,%esp的地址压入堆栈。函数的返回值存储在 eax 中。

函数调用之后:add $0x10,%esp,为了清理函数调用压入的 4 个参数,每个参数 4 字节,总共 16 字节,等于 0x10,因为 c 语言规定,谁调用,谁负责清理堆栈。

leave指令在32位汇编下相当于: :

mov %ebp,%esp;
pop %ebp 

相当于清理了堆栈中的局部变量,此时栈顶的内容为add $0x10,%esp的地址,执行 ret 指令,从栈中弹出函数返回地址,main 继续执行。

1.3 栈工作总结

总的来说,栈实现了函数调用,在栈中记录函数的返回地址,这样当函数执行完成后,调用者继续向下执行。栈用来保存函数形参,通过 ebp 加上一定的偏移就可以获得这些参数值。栈还用来存储局部变量,当函数执行完成后,这些局部变量自动被系统回收。

2、堆

堆位于栈的下方,与栈相反,堆从低地址往高地址增长。代码段和数据段(data,bss)的大小在编译时就固定了,确定了的。除了代码段,数据段,栈段,剩余的空间都可以作为堆,供程序动态使用。每一个 malloc或new 都应该有 free 和 delete 配对,否则容易造成内存泄漏,这对于像服务器端程序这样需要持续运行很久,就很关键。程序结束后,系统会回收所有资源,包括栈,和堆中已经分配但还没释放的空间。

#include 
int main(){
    int *p = malloc(sizeof(int));
    *p = 12;
    free(p);      // 每一个 malloc 都应该和一个 free 配对
    return 0;
}

上述代码中,指针 p 属于局部变量,保存在栈上,p指向的内存存储在堆中。

栈和堆一起构成堆栈段

3、 数据段

数据段的大小在编译的时候就已经确定,在运行过程中也可以通过 sbrk() 来调整数据段的大小。

3.1 bss 段

一般存放未初始化的全局数据和未初始化的静态数据,bss 段变量都初始化为0。

例如下列代码中:count,sum,grade 存储于bss 段

#include 

int count;
static int sum;
int main(){
    static int grade;
    return 0;
}

3.2 data 段

一般存放非0全局变量,非0静态变量(staic 修饰)。例如以下代码中 global, count, size 存储在该区域。

#include 

int global = 100;
static int count = 80;
int main()
{
    static int size = 2;
    return 0;
}

4、代码段

代码段的页表决定它只可读,任何尝试写的操作都会造成页错误。所以程序的机器指令和只读数据存储于代码段。

只读数据:

  • 字符串常量。
  • const 修饰的全局变量。

const 修饰的局部变量存储在栈上,它不可以直接修改,但可以间接修改:

void foo(){
	const int sum = 80;      // 局部 const 变量,存储在栈上
	sum = 99;    						 // 错误,不可以直接修改
	int *p = (int*)∑
	*p = 99;                 // 正确,可以间接修改
}

在以下代码中, 指针s保存在栈中,它保存了字符串"Hello world" 的地址,“Hello world” 本身保存在代码段中。

const 修饰的全局变量保存在代码段中,例如:con,*p1, p2, p3,*p3,但是 p1,*p2 在数据段中。

#include 

void print(char* str){
    printf("%s\n",str);
}

const int con = 18;
int m = 100;
int n=99;
int x = 1024;
const int *p1 = &m;            //  p 指向代码段中的一个地址,此时 *p 修改了,就等于修改代码段,导致错误
int *const p2 = &n;            //  p 本身是代码段中的一个地址,此时 p 被修改了,就等于修改代码段,导致错误
cont int* const p3 = &x;       //  p 本身和 p 执行的地址都位于代码段,此时,修改 p 还是修改 *p 都会段错误
int main(){
    char *s = "Hello world";
    print(s);
    return 0;
}

现在回答以下问题:

*为什么 p1 在代码段,p1 在data?

根据const 的位置,当const 在 类型前时,p1 指向的内存为常量,因而位于代码段中,p1 本身不是常量,所以存在于data段中。

*为什么 p2 在代码段,p2 在data 段?

const 在指针名前,表示 p2 本身为常量,因而位于代码段,但该指针指向的内存地址不为常量,因而位于data段中。

同理可解释为什么 p3 和 *p3 都位于代码段。

如有遗漏,欢迎指正。

你可能感兴趣的:(Linux,linux,c语言,地址空间,变量存储位置,const常量存储位置)