Linux APUE学习:4、C程序的运行时环境和存储空间布局

​ C程序的命令行参数环境表统称为程序的“**运行时环境**”。这是因为这些参数和变量在程序运行期间提供了重要的信息和配置选项,对于程序的正确执行至关重要。命令行参数是在程序启动时传递给程序的选项和参数,而环境表则包含了一些系统和用户定义的环境变量,这些变量可以影响到程序的行为。

命令行参数argv

​ 当执行一个程序时,调用exec的进程可以将命令行参数传递给该新程序

实验例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main (int argc, char **argv)
{
int i = 0;
for( ; i<argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
// ISO C和POSIX1都要求 argv[argc]是一个空指针。这就使我们可以将参数处理循环改写为如下形式,效果是相同的
for(i=0; argv[i] != NULL; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}

return 0;
}

命令行参数argv例程

​ 可知argv[0]是执行的可执行文件文件名,argv[>0]的是跟随的命令行参数(以空格为间隔),这也是Unix系统下不要以名称中带空格来命名文件,否则在调用时常常出现错误

环境表

​ 每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null 结束的C 字符串的地址。全局变量environ 则包含了该指针数组的地址:

1
extern char **environ;

​ 例如,如该环境包含5 个字符串,那它看来如下图中。其每个字符串的结尾处都显式地有一个NULL字节。我们称environ 为环境指针(environment pointer),指针数组为环境表,其中各指针指向的字符串为环境字符串。

环境指针

大多数预定义名完全由大写字母组成,但这只是一个惯例。

​ 在历史上,大多数 UNIX 系统支持 main 函数带3个参数。其中第3个参数就是环境表地址

1
int main (int argc, char *argv[], char *envp[]);

​ 因为ISO C规定 main 函数只有两个参数,而且第3个参数与全局environ 相比也没有带来更多益处,所以 POSIX.1 也规定应使用 environ 而不使用第 3个参数。通常用getenvputenv函数来访问特定的环境变量,而不是用environ 变量。但是,如果要查看整个环境,则必须使用 environ 指针。

C程序的存储空间布局

具体含义

​ Linux 进程内存管理的对象都是虚拟内存,每个进程先天就有 0-4G 的各自互不干涉的虚拟内存空间,0—3G 是用户空间执行用户自己的代码, 高 1GB 的空间是内核 空间执行 Linux 系统调用,这里存放在整个内核的代码和所有的内核模块,用户所看到和接触的都是该虚拟地址,并不是实际 的物理内存地址。

​ Linux下一个进程在内存里有三部分的数据,就是”代码段”、”堆栈段”和”数据段”。其实学过汇编语言 的人一定知道,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分是构成一个完整的执行序列的必要的部 分。

​ ”代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用相 同的代码段。”堆栈段”存放的就是子程 序的返回地址、子程序的参数以及程序的局部变量和malloc()动态申请内存的地址。而 数据段则存放程序的全局变量,静态变量及常量的内存空间。

是Linux下进程的内存布局

从历史沿袭至今,C程序一直由下列几部分组成:

  • 正文段(文本段):这是由CPU执行的机器指令部分。通常,正文段是可以共享的,所以即使是频繁执行的程序(如文本编辑器、C编译器和shell等)在存储器中也只需有一个副本。另外,文本段通常是只读的,以防止程序由于意外而修改其指令。
  • 初始化数据段:通常称为.data段、数据段,它包含了程序中需要明确赋初值的变量。即用来保存已初始化的全局变量和 static 静态变量。
  • 未初始化数据段:通常称为.bss段,这一名称来源于早期汇编程序一个操作符,意思是“由符号开始的块”(block started by symbol)。用来存放未初始化的全局变量和 static 静态变量。并且在程序开始执行之前, 就是在 main()之前,内核会将此段中的数据初始化为 0 或空指针。
  • :栈内存由编译器在程序编译阶段完成分配,进程的栈空间位于进程用户空间的顶部并且是向下增长,每个函数的每次调用 都会在栈空间中开辟自己的栈空间,函数参数、局部变量、函数返回地址等都会按照先入者为栈顶的顺序压入函数栈中, 函数返回后该函数的栈空间消失,所以函数中返回局部变量的地址都是非法的。
  • :堆内存是在程序执行过程中分配的,用于存放进程运行中被动态分配的的变量,大小并不固定,堆位于非初始化数据 段和栈之间,并且使用过程中是向栈空间靠近的(即向上增长)。当进程调用 malloc 等函数分配内存时,新分配的内存并不是该函数的 栈帧中,而是被动态添加到堆上,此时堆就向高地址扩张;当利用 free 等函数释放内存时,被释放的内存从堆中被踢 出,堆就会缩减。因为动态分配的内存并不在函数栈帧中,所以即使函数返回这段内存也是不会消失。

Linux内存管理

​ Linux 内存管理的基本思想就是只有在真正访问一个地址的时候才建立这个地址的物理映射,Linux C/C++语言的分配方式共有 3 种方式。

  1. 从静态存储区域分配。就是数据段的内存分配,这段内存在程序编译阶段就已经分配好,在程序的整个运行期间都存在, 例如全局变量,static 变量。
  2. 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。 栈内存分配运算内置于处理器的指令集中,效率很高,但是系统栈中分配的内存容量有限,比如大额数组就会把栈空间撑爆导致段错误。
  3. 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。此 区域内存分配称之为动态内存分配。动态内存的生存期由我们决定,使用非常灵活,但问题也最 多,比如指向某个内存块的指针取值发生了变化又没有其他指针指向 这块内存,这块内存就无法访问,发生内存泄露。

程序示例

关于验证C程序编译以及运行时的进程内存布局代码示例,可以到这里查看

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
#include <stdio.h>
#include <stdlib.h>

int g_var1; // save data.bss
int g_var2 = 20; // save data.data

int main(int argc, char** argv)
{
static int s_var1; // save data.bss
static int s_var2 = 20; // save datd.data

char *str = "Hello"; //save STACK and value = &"Hello"
char *ptr; //save STACK but value = ?

printf("*str = %p \n", str);
printf("*ptr = %p \n", ptr);
ptr = malloc(100);
printf("new *ptr = %p \n", ptr);

printf("[cmd args]: argv address: %p\n", argv);
printf("[ Stack ]: str address: %p\n", &str);
printf("\n");

printf("[ Heap ]: malloc adress: %p\n", ptr);
printf("\n");

printf("[ bss ]: s_var1 address: %p value: %d\n", &s_var1, s_var1);
printf("[ bss ]: g_var1 address: %p value: %d\n", &g_var1, g_var1);

printf("[ data ]: s_var2 address: %p value: %d\n", &s_var2, s_var2);
printf("[ data ]: g_var2 address: %p value: %d\n", &g_var2, g_var2);

printf("[ rodata ]: \"%s\" address: %p \n\n" ,str ,str);

printf("[ text ]: main() address: %p \n\n", main);
return 0;
}