第六章:可执行文件的装载与进程
6.1 进程虚拟地址空间
虚拟地址空间的概念
虚拟地址空间是操作系统为每个进程提供的独立地址空间。主要包括:
- 代码段
- 数据段
- 堆
- 栈
- 共享库
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int global_var = 10; // 数据段 static int static_var = 20; // 数据段
int main() { int local_var = 30; // 栈 int *heap_var = malloc(sizeof(int)); // 堆 *heap_var = 40; printf("global_var: %p\n", &global_var); printf("static_var: %p\n", &static_var); printf("local_var: %p\n", &local_var); printf("heap_var: %p\n", heap_var); printf("main: %p\n", main); free(heap_var); return 0; }
|
编译和运行命令:
1 2 3 4 5
| gcc virtual_space.c -o virtual_space
./virtual_space
|
内存布局
进程的内存布局包括:
- 代码段(.text)
- 数据段(.data)
- BSS段(.bss)
- 堆(heap)
- 栈(stack)
示例代码:
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
| #include <stdio.h> #include <stdlib.h> #include <unistd.h>
// 代码段 void func() { printf("Function address: %p\n", func); }
// 数据段 int global_init = 10;
// BSS段 int global_uninit;
int main() { // 栈 int stack_var = 20; // 堆 int *heap_var = malloc(sizeof(int)); *heap_var = 30; printf("global_init: %p\n", &global_init); printf("global_uninit: %p\n", &global_uninit); printf("stack_var: %p\n", &stack_var); printf("heap_var: %p\n", heap_var); printf("func: %p\n", func); free(heap_var); return 0; }
|
编译和运行命令:
1 2 3 4 5
| gcc memory_layout.c -o memory_layout
./memory_layout
|
6.2 装载的方式
静态装载
静态装载的特点:
- 一次性加载
- 固定地址
- 无重定位
示例代码:
1 2 3 4 5 6
| #include <stdio.h>
int main() { printf("Program loaded at: %p\n", main); return 0; }
|
编译和运行命令:
1 2 3 4 5
| gcc -static static_load.c -o static_load
./static_load
|
动态装载
动态装载的特点:
- 按需加载
- 地址无关
- 支持重定位
示例代码:
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
| #include <stdio.h> #include <dlfcn.h>
int main() { // 动态加载库 void *handle = dlopen("./libdynamic.so", RTLD_LAZY); if (!handle) { fprintf(stderr, "dlopen failed: %s\n", dlerror()); return 1; } // 获取函数地址 void (*func)() = dlsym(handle, "func"); if (!func) { fprintf(stderr, "dlsym failed: %s\n", dlerror()); dlclose(handle); return 1; } // 调用函数 func(); dlclose(handle); return 0; }
|
编译和运行命令:
1 2 3 4 5
| gcc dynamic_load.c -ldl -o dynamic_load
./dynamic_load
|
6.3 从操作系统角度看可执行文件的装载
进程的创建
进程创建的过程:
- 创建虚拟地址空间
- 读取可执行文件头
- 建立虚拟地址空间与可执行文件的映射关系
- 设置CPU指令寄存器
示例代码:
1 2 3 4 5 6 7 8 9 10
| #include <stdio.h> #include <unistd.h> #include <sys/types.h>
int main() { pid_t pid = getpid(); printf("Process ID: %d\n", pid); printf("Program loaded at: %p\n", main); return 0; }
|
编译和运行命令:
1 2 3 4 5
| gcc process_create.c -o process_create
./process_create
|
页错误
页错误处理:
- 缺页中断
- 页面调入
- 页面映射
示例代码:
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
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h>
#define PAGE_SIZE 4096
int main() { // 分配内存 char *ptr = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (ptr == MAP_FAILED) { perror("mmap"); return 1; } // 写入数据(触发页错误) ptr[0] = 'A'; printf("Memory allocated at: %p\n", ptr); printf("First byte: %c\n", ptr[0]); // 释放内存 munmap(ptr, PAGE_SIZE); return 0; }
|
编译和运行命令:
1 2 3 4 5
| gcc page_fault.c -o page_fault
./page_fault
|
实践练习
- 虚拟地址空间实验
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
| cat > vaddr_test.c << EOL #include <stdio.h> #include <stdlib.h>
int global = 10; static int static_var = 20;
int main() { int local = 30; int *heap = malloc(sizeof(int)); *heap = 40; printf("global: %p\n", &global); printf("static_var: %p\n", &static_var); printf("local: %p\n", &local); printf("heap: %p\n", heap); printf("main: %p\n", main); free(heap); return 0; } EOL
gcc vaddr_test.c -o vaddr_test ./vaddr_test
|
- 内存布局实验
1 2 3 4 5
| pmap -x $$
cat /proc/$$/maps
|
- 页错误实验
思考题
- 什么是虚拟地址空间?它有什么作用?
- 进程的内存布局包括哪些部分?
- 静态装载和动态装载的区别是什么?
- 操作系统如何创建进程?
- 什么是页错误?如何处理页错误?
参考资料
- 《程序员的自我修养:链接、装载与库》
- Linux 内核文档
- System V ABI
- ELF 文件格式规范
- GNU Binutils 文档