以test.c为例,分析一个进程生成一个可执行文件、装入虚拟内存的过程:
test.c代码如下:
#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 = "zxy";
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;
}
生成可执行文件的过程:
预处理(Prepressing)—>编译(Compilation)—>汇编(Assembly)—>链接(Linking)
-
gcc -E test.c -o test.i命令,可以查看预处理之后的test.i文件,预处理器把所有的头文件替换为实际文件的内容,并去掉了注释等等。
-
gcc -S test.c -o test.s命令,可以查看编译后的汇编代码:
-
接下来是汇编,将编译后的汇编代码翻译为机器码,几乎每一条汇编指令对应一句机器码。下图为整个过程:
64位系统下进程的内存布局
对于64位的系统,理论上,64bit内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF(16位十六进制数),这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。
- Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF(user space)和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF(kernel space),其余的都是unused space,user space 也就是用户区由以下几部分组成:代码段,数据段,BSS段,heap,stack。
查看该进程的内存映射信息:
cat /proc/pid/maps
- 可以看到在heap之前有五个内存区域,通过查看其权限,不难看出第二个是可执行的,所以存放的是代码段,可以看到代码段的存放起始地址。最后一个可写可读,所以存放的是数据段,但只存放全局和静态变量,局部变量存放在栈里。
查看该进程的节信息:
readelf -S test
查看该进程的段信息:
readelf -l test
-
在这里可以看到各个load段中对应的节。
-
在映射时分配给数据段的大小会大于实际大小,最后一个load段多出来的部分分给了bss段0x288-0x27c=0xc 与bss节的大小相等,不专门给bss一个专门的段去映射。
图解:
- test.c运行结果分析:
-
从程序的入口地址main 0x55ee7bfe81c9 可以看出,maps文件的第二行确实是程序的代码段。
-
a、b都是全局变量,a未定义(bss),b已定义(data),查看其地址存储在数据段,maps文件的第二行确实是程序的数据段。
-
局部变量n,存放在stack内。
-
动态内存p1,存放在堆段。
-
const常量s存放在栈里。
打印该进程虚拟空间字段的模块:hh.c
#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");
```csharp
obj-m:=hh.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-5.15.0-86-generic
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
运行命令:
make
sudo insmod hh.ko pid=XX
dmesg
运行截图:
可以看出,与我们test.c的运行结果相对应。