Linux可执行文件与进程的虚拟地址空间
一个可执行文件被执行的同时也伴随着一个新的进程的创建。Linux会为这个进程创建一个新的虚拟地址空间,然后会读取可执行文件的文件头,建立虚拟地址空间与可执行文件的映射关系,然后将CPU的指令指针寄存器设置成可执行文件的入口地址,然后CPU就会从这里取指令执行。
一个可执行文件包含可被CPU执行的指令和待处理的数据,上CPU之前,指令和数据全部被翻译成成二进制的形式。在可执行的文件的内部,划分出了一些专门的段,如代码段,数据段,BSS段等。代码段中存放的是可执行的二进制指令,数据段存放初始化过的变量,BSS段存放未初始化的变量,从装载的角度,把这些段称为segment。
打印进程的地址信息
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int a;
int b=1;
int main()
{
int n = 0;
char *p1 = NULL;
char *p2 = NULL;
const int s = 10;
p1 = (char*)malloc(200);
p2 = "super-lzx";
printf("PID is:%d\n\n",getpid());
printf("main %p\n",main);
printf("未初始化 a %p\n",&a);
printf("初始化 b %p\n",&b);
printf("局部变量 n %p\n",&n);
printf("动态内存 p1 %p\n",p1);
printf("常量 s %p\n",&s);
printf("常字符串 p2 %p\n",p2);
pause();
return 0;
}
Proc目录下的进程虚拟地址空间布局
Linux在装载可执行文件的时候,会将这些segment映射到进程的地址空间中。映射的时候,这里面的segment会对应一个VMA。Linux将进程虚拟地址空间中的一个段叫做虚拟内存区域(VMA)。在/proc目录下,可以查看一个进程的虚拟地址空间,通过命令cat /proc/pid/maps
第二行为可读可执行,三四行为只读,第五行为可读可写。
所以,操作系统实际上并不关心可执行文件各个段所包含的的实际内容,OS只关心一些跟装载相关的问题,最主要的是段的权限(可读,可写,可执行)。
ELF文件中,段的权限往往只有为数不多的几种组合,基本上就3种:
1.以代码段为代表的权限为可读可执行的段
2.以数据段和BSS段为代表的权限为可读可写的段
3.以只读数据段为代表的权限为只读的段
根据上面test.c文件打印出的结果进行分析,程序的入口地址main 0x556557ea01c9可以看出,maps文件的第二行就是程序的代码段。a,b为全局变量,a为定义(bss)
,b已定义(data),maps文件的三四行为程序的数据段。局部变量n存放在stack,动态内存p1,存放在堆段,donst常量s存放在栈里。
ELF可执行文件中有两个概念,分别是段(segment)和节§。通过readelf -S name.elf
可以查看ELF可执行文件的节头表,这里面有所有节的信息
在将目标文件链接成可执行文件的时候,链接器会尽量把相同权限属性的段分配在同一空间。比如可读可执行的段都放在一起,这种段的典型是代码段;可读可写的段都放在一起,这种段的典型是数据段。在ELF中,把这些属性相似的,又连在一起的段叫做一个“segment”,而系统正是按照“segment”而不是“p”来映射可执行文件的。
可以使用命令 readelf -l name.elf
来查看ELF的段。在ELF的程序头表,保存着segment的信息
下面是段与节的归属关系,
可以看到这个可执行文件中共有12个segment。从装载的角度看,我们只关心4个“LOAD”型的segment,因为只有它是需要被映射的,其他诸如“NOTE”,"GNU_STACK"都是在装载时起辅助作用的。
ELF将相同或者相似属性的p合并为一个segment并映射到一个VMA中,是为了减少页面内部碎片,以节省内存空间的使用。因为在有了虚拟存储机制以后,装载的时候采用页映射的方式。Intel系列的处理器,页尺寸最小是4096个字节,也就是4KB。当写的程序很小的时候,每个p可能只有几十或者几百个字节,如果每个p都占用一个页的话,对内存的浪费是海量的。所以在将目标文件链接成可执行文件的时候,链接器会尽量把相同或相似权限属性的p分配在同一空间,在程序头表中,将一个或多个属性类似的p合并为一个segment,然后在装载的时候,将这个segment映射到进程虚拟地址空间中的一个VMA中。
很明显,属性相同或相似的p会被归类到一个segment,并且被映射到同一个VMA。
最后我们通过一个打印变量地址的小程序进行验证,仔细观察没有初始化的全局变量和一些静态变量的线性地址。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int pid = 1;
module_param(pid, int, 0644); // 模块参数,表示目标进程的PID
// 打印目标进程的虚拟空间字段
static int print_vm_areas(void) {
struct task_struct *task;
struct mm_struct *mm;
struct vm_area_struct *vma;
printk(KERN_INFO "print_vm_areas: begin.\n");
// 通过进程 PID 找到对应的进程的 task_struct 结构
task = pid_task(find_vpid(pid), PIDTYPE_PID);
if (!task) {
printk(KERN_ERR "print_vm_areas: cannot find process with PID %d\n", pid);
return -EINVAL;
}
// 找到进程的 mm_struct 结构,即内存描述符
mm = task->mm;
if (!mm) {
printk(KERN_ERR "print_vm_areas: failed to get mm_struct of process with PID %d\n", pid);
return -EINVAL;
}
// 打印进程的名称和 PID
printk(KERN_INFO "Executable name: %s, PID: %d\n", task->comm, pid);
// 打印进程的代码段、数据段和堆栈地址范围
printk(KERN_INFO "Code: 0x%lx - 0x%lx\n", mm->start_code, mm->end_code);
printk(KERN_INFO "Data: 0x%lx - 0x%lx\n", mm->start_data, mm->end_data);
printk(KERN_INFO "Heap: 0x%lx-0x%lx\n", mm->start_brk, mm->brk);
printk(KERN_INFO "Stack: 0x%lx\n", mm->start_stack);
// 遍历进程的内存映射区间并打印
printk(KERN_INFO "Virtual memory areas:\n");
for (vma = mm->mmap; vma; vma = vma->vm_next) {
printk(KERN_INFO "%16lx-%16lx %c%c%c%c\n",
vma->vm_start,
vma->vm_end,
(vma->vm_flags & VM_READ) ? 'r' : '-',
(vma->vm_flags & VM_WRITE) ? 'w' : '-',
(vma->vm_flags & VM_EXEC) ? 'x' : '-',
(vma->vm_flags & VM_SHARED) ? 's' : 'p');
}
printk(KERN_INFO "print_vm_areas: end.\n");
return 0;
}
// 模块初始化函数,在加载模块时调用
static int __init print_init(void) {
printk(KERN_INFO "print_vm_areas: module loaded.\n");
print_vm_areas(); // 打印目标进程的虚拟空间字段
return 0;
}
// 模块退出函数,在卸载模块时调用
static void __exit print_exit(void) {
printk(KERN_INFO "print_vm_areas: module unloaded.\n");
}
module_init(print_init);
module_exit(print_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(intree, "Y");
可以看到这个结果与我们的test运行的结果部分是一一对应的。