本文是对这篇文章的一个补充。
首先我们知道:
- 程序是静态的
- 源码经过预处理、编译、汇编、链接变成可执行文件,该可执行文件可以认为就是程序
- 可执行文件能够多次执行,但并不意味着每次使用的资源是一样的
- 进程是动态的
- 当程序加载到内存中开始运行,直到运行结束,这样从开始到结束的过程就是进程
- 程序位于存储设备上,此时不叫做进程,当加载到内存上开始执行才转变为进程
进程空间
上图给出了进程空间的一个模型。在 C 语言中如果利用 %p 输出变量地址的话,会出现该变量的对应地址,但其实该地址可能并不意味着真正的变量地址。通常情况下,程序到进程的过程为:
上图表明,硬盘上的程序和数据之间还隔着虚拟内存和 MMU(memory manager unit),硬盘上的程序通过虚拟内存获得内存资源在虚拟内存上的一个映射,然后 MMU 再将虚拟内存中的资源映射到实际的内存单元,从而获得了真实的内存资源,得以运行。
对于一个进程来说,会存在几部分内容。
命令行参数和环境变量
#include <stdio.h>
int main(int argc, char *argv[], char **env)
{
printf("The number of arguments is %d\n",argc);
printf("******************\n");
for(int i=0; i<=argc; i++)
{
printf("The %d argument is %s\n",i,argv[i]);
}
printf("******************\n");
while(*env++)
printf("%s\n",*env);
printf("******************\n");
return 0;
}
对于上边的程序,在命令行中的运行结果为:
C:\Users\wood\Desktop\Qtest\build-test-Desktop_Qt_5_8_0_MinGW_32bit-Debug\debug>test.exe 10 20 30
The number of arguments is 4
******************
The 0 argument is test.exe
The 1 argument is 10
The 2 argument is 20
The 3 argument is 30
The 4 argument is (null)
******************
APPDATA=C:\Users\wood\AppData\Roaming
AWE_DIR=D:\Awesomium_SDK\1.6.6\
...
******************
从上边的程序中,我们可以看出:
- 带有参数的 main 函数可以在命令行中实现调用
- argc 表示参数个数,是包含可执行程序在内的所有参数
- argv 为实际的参数,实际参数个数比 argc 多 1,可执行参数默认为参数 0,null 默认为最后一个参数
- env 表示当前系统内的环境变量
- 上边所有的内容在程序运行时自动加载到进程空间中。
栈和堆
命令行参数下的是 stack 和 heap 空间,从箭头指向上来看,stack 空间是往下压栈的,而 heap 是往上压栈的:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a;
int b;
printf("%p\n",&a);
printf("%p\n",&b);
char *p1 = (char *)malloc(100);
char *p2 = (char *)malloc(100);
printf("%p\n",p1);
printf("%p\n",p2);
return 0;
}
结果为:
0060FEA4
0060FEA0
00F60E70
00F60EE0
从上边的结果可以看出 b 的地址比 a 的地址低,p2 的地址比 p1 的地址高。同样进行函数调用时,函数内部变量也会加入压栈的顺序。
同时如果会使用到外部的函数库的话,还会将函数库的内容加载到内存中,对应在 stack 和 heap 之间的一段区域。
未初始化数据段,初始化数据段,代码段
#include <stdio.h>
int a;
int b = 10;
int main()
{
static int c;
static int d = 20;
char str[10] = "hello";
int arr[5] = {1,2,3,4,5};
return 0;
}
对于上边程序中存在的变量:
- 未初始化的普通全局变量和静态变量存储在 uninitialized data,如变量 a,c
- 初始化的普通全局变量和静态变量存储在 initialized data 中的 rw 段(read-write),如变量 b,d
- 字符串常量存储在 uninitialized data 中的 ro 段(read-only),如 “hello”
- 还有一些常量存储在 text 段,如 {1,2,3,4,5}
归纳为:
变量 | 进程空间分布 | 说明 | |
Command-line arguments and environment variables | 命令行参数和环境变量 | ||
普通局部变量 | stack | 栈 | |
dynamic lib | 动态库 | ||
alloc 函数申请空间 | heap | 堆 | |
普通全局变量 静态变量 |
未初始化 | uninitialized data | 未初始化数据段 |
初始化 | initialized data(rw) | 初始化数据 rw 段 | |
常量 | initialized data(ro) | 初始化数据 ro 段 | |
text | 代码段 |
函数调用时的压栈和出栈
#include <stdio.h>
int func(int x)
{
return x;
}
int foo(int x)
{
return func(x);
}
int main()
{
int a = 10;
printf("%d",foo(a));
return 0;
}
在上边的程序中,先是 main 函数发生调用,然后在 main 函数中又调用函数 foo,之后在函数 foo 中又调用 func。整个过程会发生一系列的入栈和出栈,具体顺序为:
调用 main 函数时,入栈:
- 操作系统运行状态
- 返回地址
- main() 参数
调用 foo 函数时,入栈:
- main() 运行状态
- 返回地址
- foo() 的参数
调用 func 函数时,入栈:
- foo() 运行状态
- 返回地址
- func() 的参数
然后再按照相反的顺序出栈,从而完成整个进程。