首先看linux进程在32位处理器下的虚拟空间内存布局,以i386 32位机器为例
x86_32 32位处理器进程虚拟地址空间布局
每个用户进程的虚拟地址空间为0x0—0xC0000000也就是3GB,其中0x0—0x08000000 128MB地址空间用于捕获空指针,用户空间分为代码段,堆,mmap区,栈。
堆的起始地址start_brk依据代码段和数据段的大小确定,堆从低地址往高地址增长,mmap区从高地址往低地址增长当两个区域相撞时则区域耗完,mmap区基地址最大为A0000000,mmap_base — 0XC0000000为栈的空间,其中两个区间之间都有一个缝隙。
内核空间为0XC0000000—0xFFFFFFFF 1GB, 如果物理内存大于896MB,则内核的虚拟地址0xC0000000—0xF8000000 和 物理内存0—896MB对等映射。所以内核 为了访问大于896MB的物理内存需要设置一段虚拟区域映射其他的物理内存,这段虚拟地址叫做高端内存,VMALLOC区用函数vmalloc分配内存页面不保证连续,持久映射用函数kmap建立映射,这段映射是长期映射,固定映射是虚拟地址和物理内存固定的地址进行映射。
x86_64的进程地址空间布局就不一样了,intel的64位处理器地址线最多52根,也就是支持2^52的地址排布,理论上最大支持4096TB的内存,但是不同的处理器地址线个数不一样,有36,40,46 ,52等。根据英特尔手册查看,实际支持的物理内存大多是64GB,最多的是至强处理器支持16TB的物理内存。所以以现在的物理内存大小对于虚拟地址空间完全够用。英特尔64位处理器支持最大的线性地址是48位,也就是mmu从虚拟地址映射到物理地址只使用了48位,所以[48:63]位是扩展位,必须和第47位值一样,否则会#GP(General Protection)异常。所以x86_64的线性地址空间是0x0—0x00007FFFFFFFFFFF, 0xFFFF800000000000—0xFFFFFFFFFFFFFFFFFFFF。linux在x86_64下的经典布局如下图
x86_64 64位处理器进程地址空间布局
用户空间分区一致,区别就是地址空间变大了,内核空间取消了高端内存,因为内核空间的地址空间完全可以访问全部物理内存。
下面以32位处理器为例看linux内核如何建立用户进程空间的内存布局的,fork调用是复制父进程的struct mm_struct的内存描述符不需要重新建立布局,而建立新的内存布局是通过加载二进制可执行文件。execve函数族加载可执行文件是将当前进程镜像替换为新的进程映像,我们看一下linux加载二进制文件建立布局的流程,只分析内存布局代码,其它的会专门写一篇二进制文件加载的分析。
linux内核提供了sys_execve函数,对应用户态的execv函数族,具体代码如下:
/*
* sys_execve() executes a new program.
*/
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;
filename = getname((char __user *) regs.ebx); //获取文件名
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
®s);
.....
.....
}
此函数获取可执行文件路径然后调用了do_execve
/*
* filename:可执行文件路径
* argv:运行程序所需的参数
* envp:运行程序所需的环境变量
*/
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
struct linux_binprm *bprm; //可执行文件相关参数结构体
struct file *file;
int retval;
int i;
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); //分配bprm结构
if (!bprm)
goto out_ret;
file = open_exec(filename); //获取文件指针
bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);//当前进程内存的最大地址
bprm->mm = mm_alloc(); //分配内存描述符
bprm->argc = count(argv, bprm->p / sizeof(void *)); //计算传入的参数个数
if ((retval = bprm->argc) < 0)
goto out_mm;
bprm->envc = count(envp, bprm->p / sizeof(void *)); //计算环境变量个数
if ((retval = bprm->envc) < 0)
goto out_mm;
retval = prepare_binprm(bprm); //读取elf文件的头,放入bprm->buf
retval = search_binary_handler(bprm,regs);
......
......
out_kfree:
kfree(bprm);
out_ret:
return retval;
}
然后进入search_binary_handler函数处理,这个函数才调用到了真正装载二进制文件的函数,通过函数指针调用:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
int try,retval;
struct linux_binfmt *fmt;
retval = security_bprm_check(bprm);
if (retval)
return retval;
retval = -ENOENT;
for (try=0; try<2; try++) {
read_lock(&binfmt_lock); //读加锁
//遍历linux_binfmt结构
for (fmt = formats ; fmt ; fmt = fmt->next) {
//获取加载二进制文件的函数指针,这个函数会对应到fs/binfmt_elf.c的 load_elf_binary函数
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
if (!fn)
continue;
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fn(bprm, regs); //调用加载二进制文件的函数
.......
}
.........
.........
return retval;
}
下面看真正的加载elf文件函数load_elf_binary,这个函数比较长,很多涉及到处理elf文件的细节,先不看这些细节,主要看此函数调用的几个函数:
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{
struct file *interpreter = NULL; /* to shut gcc up */
unsigned long load_addr = 0, load_bias = 0;
int load_addr_set = 0;
char * elf_interpreter = NULL;
unsigned int interpreter_type = INTERPRETER_NONE;
unsigned char ibcs2_interpreter = 0;
unsigned long error;
struct elf_phdr *elf_ppnt, *elf_phdata;
unsigned long elf_bss, elf_brk;
int elf_exec_fileno;
int retval, i;
unsigned int size;
unsigned long elf_entry, interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
elf_ppnt = elf_phdata;
elf_bss = 0; //bss段结束地址
elf_brk = 0; //堆起始地址
start_code = ~0UL; //代码段起始地址初始0xFFFFFFFF
end_code = 0; //代码段结束地址
start_data = 0; //数据段起始地址
end_data = 0; //数据段结束地址
/* 此处可以看出execv函数族是将当前进程内存布局替换为新进程的内存布局*/
current->mm->start_data = 0; //重置当前进程数据段起始地址
current->mm->end_data = 0; //重置当前进程数据段结束地址
current->mm->end_code = 0; //重置当前进程代码段结束地址
current->mm->mmap = NULL; //重置当前进程mmap区基地址
//设置mmap的基地址
arch_pick_mmap_layout(current->mm);
//处理栈空间
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
if (retval < 0) {
send_sig(SIGKILL, current, 0);
goto out_free_dentry;
}
//设置栈的起始地址
current->mm->start_stack = bprm->p;
/* 处理elf文件各个段,确定代码段,数据段,bss段的起始和结束地址*/
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (elf_ppnt->p_flags & PF_R)
elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W)
elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X)
elf_prot |= PROT_EXEC;
elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
vaddr = elf_ppnt->p_vaddr; //当前段的起始地址
//建立vma结构,也就是每个段一个vma
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags);
if (BAD_ADDR(error)) {
send_sig(SIGKILL, current, 0);
goto out_free_dentry;
}
k = elf_ppnt->p_vaddr; //每个段的起始地址,如果是代码段,则表示程序入口地址
if (k < start_code) //start_code初始是0Xffffffff,所以k一定小于
start_code = k; //确定程序入口地址
if (start_data < k) //确定数据段起始地址
start_data = k;
//p_filesz表示此段在文件中的大小 p_filesz <= p_memsz,因为BSS段是未初始化全局变量,在编译好的
//目标文件中BSS段不占用文件的内存,只有加载到内存时,BSS段才会占用内存空间初始化为0
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss)
elf_bss = k; //BSS段的起始地址
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k; //确定代码段结束地址
if (end_data < k)
end_data = k; //数据段结束地址
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; //p_memsz表示此段在内存中的大小,如果当前是data段则计算出加上BSS段的结束地址
if (k > elf_brk)
elf_brk = k; //堆起始地址
}
//确定最终的各段地址,load_bias在动态链接时有用到,暂不分析
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;
//设置堆空间
retval = set_brk(elf_bss, elf_brk);
//确定代码段数据段和栈
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
out:
kfree(loc);
out_ret:
return retval;
/* error cleanup */
out_free_dentry:
allow_write_access(interpreter);
if (interpreter)
fput(interpreter);
out_free_interp:
kfree(elf_interpreter);
out_free_file:
sys_close(elf_exec_fileno);
out_free_fh:
if (files) {
put_files_struct(current->files);
current->files = files;
}
out_free_ph:
kfree(elf_phdata);
goto out;
}
再看如何确定mmap的基地址arch_pick_mmap_layout,这是一个体系结构相关的函数
#define MIN_GAP (128*1024*1024)
#define MAX_GAP (TASK_SIZE/6*5)
static inline unsigned long mmap_base(struct mm_struct *mm)
{
unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur;
unsigned long random_factor = 0;
if (current->flags & PF_RANDOMIZE)
random_factor = get_random_int() % (1024*1024); //随机数在1MB以内
if (gap < MIN_GAP)
gap = MIN_GAP; //最小128MB
else if (gap > MAX_GAP)
gap = MAX_GAP; //最大512MB
return PAGE_ALIGN(TASK_SIZE - gap - random_factor);//0xc0000000 - 128MB - rand —— 0xc0000000 - 512MB - rand
}
void arch_pick_mmap_layout(struct mm_struct *mm)
{
//经典布局,堆空间只有不到1GB,mmap基地址0X40000000并且向高地址增长
if (sysctl_legacy_va_layout ||
(current->personality & ADDR_COMPAT_LAYOUT) ||
current->signal->rlim[RLIMIT_STACK].rlim_cur == RLIM_INFINITY) {
mm->mmap_base = TASK_UNMAPPED_BASE;
mm->get_unmapped_area = arch_get_unmapped_area;
mm->unmap_area = arch_unmap_area;
} else { //新的布局mmap向低地址增长,堆向高地址增长
mm->mmap_base = mmap_base(mm); //mmap基地址B8000000 ~ A0000000 - 随机数
mm->get_unmapped_area = arch_get_unmapped_area_topdown;
mm->unmap_area = arch_unmap_area_topdown;
}
}
再看确定栈指针的函数,栈分为向上增长和向下增长,默认向下增长:
int setup_arg_pages(struct linux_binprm *bprm,
unsigned long stack_top,
int executable_stack)
{
unsigned long stack_base;
struct vm_area_struct *mpnt;
struct mm_struct *mm = current->mm;
int i, ret;
long arg_size;
stack_base = arch_align_stack(stack_top - MAX_ARG_PAGES*PAGE_SIZE);//stack 基地址 0XC0000000 - rand - 128MB
stack_base = PAGE_ALIGN(stack_base); //页面对齐
bprm->p += stack_base; //128MB - 4B + 0XC0000000 - rand - 128MB,bprm->p也是栈的起始地址
mm->arg_start = bprm->p; //运行参数起始地址
arg_size = stack_top - (PAGE_MASK & (unsigned long) mm->arg_start);//运行参数大小
return 0;
}
至此内存布局就完成了,关于elf文件加载写的不详细,会单独写一篇elf文件加载的详细过程。