打印进程的虚拟地址空间

文章详细解释了Linux中可执行文件与进程的虚拟地址空间的关系,包括ELF文件结构、代码段、数据段、BSS段以及段和节的概念,描述了映射过程和/proc目录下的地址空间查看方法,以及如何通过程序验证内存区域的分配情况。
摘要由CSDN通过智能技术生成

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的信息
![请添加图片描述](https://img-blog.csdnimg.cn/d9b9fc3bfc404d8b92f1b94d329df513.png

下面是段与节的归属关系,
在这里插入图片描述
可以看到这个可执行文件中共有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运行的结果部分是一一对应的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值