Linux源码解析——可执行文件从生成到加载

   首先我们拿一个很简单的程序test1.c为例
#include"test1.h"
//#include<stdio.h>
int del()
{
        return 0;
}
int main(void)
{
    int a = DIV;
    const char* p =(char*)del;
 //   printf("del 0x%lx\n",(u_int64_t)p);
    while(1);
    return 0;
}
   将这么一个程序编译成可执行文件需要四个步骤  1. 预处理  2.编译 3.汇编 4.链接,接下来就逐步进行分析。

1.预处理: gcc -E test1.c -o test1.i 生成的test1.i

# 1 "test1.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test1.c"
# 1 "test1.h" 1
# 2 "test1.c" 2

int del()
{
 return 0;
}
int main(void)
{
    int a = 5;
    const char* p =(char*)del;

    while(1);
    return 0;
}

这里就能很清楚的看出预编译的作用 条件编译,头文件包含,宏替换的处理。
2.编译: gcc -S test1.c -o test1.s ,生成的test1.s(这里就贴部分代码)

main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$5, -12(%rbp)
	leaq	del(%rip), %rax
	movq	%rax, -8(%rbp)

所以编译就是生成汇编文件
3.汇编:gcc test1.c -o test1.o,生成二进制文件,但是此时的二进制文件并不能直接运行(还没有链接)(比如说上面的 printf)在这个文件里没有被链接,所以还需要第四步
4.链接:链接生成可执行文件(是一个elf文件,elf的结构就不在这里介绍了,以下是elf头部信息)
在这里插入图片描述
再生成可执行程序之后,接着考虑linux系统是如何将其运行起来的,

首先调用fork产生一个子进程,然后再执行exec函数。
do_execve  -----> do_execveat_common```
do_execveat_common:
打开该可执行文件
	file = do_open_execat(fd, filename, flags);
	bprm->file = file;
初始化 bprm->mm ,在后面会对current->mm进行替换
	retval = bprm_mm_init(bprm);
拷贝环境变量和参数
	bprm->exec = bprm->p;
	retval = copy_strings(bprm->envc, envp, bprm);
	retval = copy_strings(bprm->argc, argv, bprm);
do_execve  -----> do_execveat_common---->exec_binprm---->search_binary_handler 
---->fmt->load_binary(bprm)------>load_elf_binary
load_elf_binary:
解析elf头部,最重要的是 Start of program headers 以及   Entry point address,
 Start of program headers: phdr的起始地址。 
 Entry point address: 如果不包含解释器,那么就是程序的入口地址。
	loc->elf_ex = *((struct elfhdr *)bprm->buf);
解析program headers,
    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);

在这里插入图片描述

if (elf_ppnt->p_type == PT_INTERP)
{
	...
	retval = kernel_read(bprm->file, elf_ppnt->p_offset,
					     elf_interpreter,
					     elf_ppnt->p_filesz);
	...
	interpreter = open_exec(elf_interpreter);
	...				     
	retval = kernel_read(interpreter, 0,
					     (void *)&loc->interp_elf_ex,
					     sizeof(loc->interp_elf_ex));					     
    ...			     
}
这一步就是加载解释器,首先从program header头中找到类型为INTERP的那段,通过其offset和size找到具体在文件中的内容,其实就是一串字符串(/lib64/ld-linux-x86-64.so.2,然后打开该文件(也是一个elf),读取其elf头部
if (elf_interpreter) {
	...
	interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
						   interpreter);
    ...
}
这里同理是读取解释器的program header

retval = flush_old_exec(bprm);
将原来的current->mm 替换成初始化完了的brmp->mm,do_close_on_exec(current->files);关闭原来打开的文件
也就是说原来fork的用户空间都会被清除,包括(代码段,数据段,堆,栈)。



if (elf_ppnt->p_type != PT_LOAD)
			continue;
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
				elf_prot, elf_flags, total_size);
将type是PT_LOAD的段通过mmap映射到用户空间,因为这里设置了start地址:load_bias + vaddr,也就是说如果mmap的[load_bias + vaddr,load_bias + vaddr+total_size]这段空间没有被映射,那么会直接映射到该地址上。
因为mm是刚初始化的,所以会直接加载到load_bias + vaddr。

k = elf_ppnt->p_vaddr;
if (k < start_code)
	start_code = k;
if (start_data < k)
	start_data = k;
  第一个PT_LOAD的起始地址就是start_code
最后一个PT_LOAD的起始地址就是start_data
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss)
	elf_bss = k;
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
	end_code = k;
if (end_data < k)
	end_data = k;
	第一个PT_LOAD的文件结束地址就是end_code
	最后一个PT_LOAD的文件结束地址就是end_data也就是elf_bss(bss一般在最后)
loc->elf_ex.e_entry += 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;
都要加上load_bias,才是真正的映射到用户空间的虚拟地址
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;
重新填充mm中的一些字段

if (elf_interpreter) {
	elf_entry = load_elf_interp(&loc->interp_elf_ex,
					    interpreter,
					    &interp_map_addr,
					    load_bias, interp_elf_phdata);
if (!IS_ERR((void *)elf_entry)) 
{
	interp_load_addr = elf_entry;
	elf_entry += loc->interp_elf_ex.e_entry;
	...
} else {
	elf_entry = loc->elf_ex.e_entry;
	...
}
加载解释器,将其LOAD段通过mmap加载到用户空间,并将程序的入口地址设置为新的入口地址(根据load_elf_interp的返回值)。

start_thread(regs, elf_entry, bprm->p);  重新设置内核堆栈中(在调用系统exec时,会保存现场,将寄存器保存在内核堆栈中),这里就是要修改内核堆栈中保存的sp和pc值,在系统调用结束后,就直接跳转到elf_entry。
这里有一个疑问:struct pt_regs *regs = current_pt_regs(); 是怎么找到内核堆栈中的寄存器位置的???

这样一个可执行文件就能成功的运行了,这里假设没有解释器的情况下,重新走一遍流程

  1. ./test1
    
  2. fork一个子进程,调用exec
    
  3. 在新的进程中打开test1文件,解析该elf文件头部
    
  4. 清除原来的mm以及file,并替换成初始化完的mm。
    
  5. 将elf program header 中的LOAD段通过mmap映射到用户空间指定位置
    
  6. 将内核堆栈中pc寄存器修改为elf头部获取的相关入口地址,这样就实现了跳转。
    

接下来通过一个实例来演示一遍

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int a1 = 1;
int add()
{
    return 1;
}
int main(int argc, char *argv[])
{
    pid_t pid=fork();
        if ( pid < 0 ) {
            fprintf(stderr,"错误!");
        } else if( pid == 0 ) {
            //execv("/home/yu/user/test/test1",NULL);
            while(1);
        } else {
            printf("父进程空间,子进程pid为%d\n",pid);
            const char* p =(char*)add;
            printf("0x%lx\n",(u_int64_t)p);
            const char* p1 =(char*)&a1;
            printf("a1 0x%lx\n",(u_int64_t)p1);
            while(1);
        }
        // 可以使用wait或waitpid函数等待子进程的结束并获取结束状态
        while(1);
}

通过打印可以知道main的地址是0x56060c829745,通过反编译可以看到,main的offset确实是0x745
在这里插入图片描述
在这里插入图片描述
而通过program header 的第一个LOAD程序是从offset 0 - 0xa77的,所以我们看一下offset0的内存,也就是虚拟地址0x56060c829000
在这里插入图片描述
确实是从ELF的头开始拷贝。
这个是父进程,那么看看子进程,这里只执行了fork,所以按理说这个0x56060c829000这个虚拟地址显示的也是这个ELF文件,看看是不是这样
在这里插入图片描述
确实是一样的,这时候我们修改代码再来一遍

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int a1 = 1;
int add()
{
    return 1;
}
int main(int argc, char *argv[])
{
    pid_t pid=fork();
        if ( pid < 0 ) {
            fprintf(stderr,"错误!");
        } else if( pid == 0 ) {
            execv("/home/yu/user/test/test1",NULL);
            while(1);
        } else {
            printf("父进程空间,子进程pid为%d\n",pid);
            const char* p =(char*)main;
            printf("0x%lx\n",(u_int64_t)p);
            const char* p1 =(char*)&a1;
            printf("a1 0x%lx\n",(u_int64_t)p1);
            while(1);
        }
        // 可以使用wait或waitpid函数等待子进程的结束并获取结束状态
        while(1);
}

加上了一句execv("/home/yu/user/test/test1",NULL);在这里插入图片描述
在子进程中,老的代码段已经清空了 ,就是flush_old_exec,那在看看新的ELF加载在了哪里
在这里插入图片描述
里面一个函数del 地址 0x55db3a12f64a,看看反编译是否在offset=0x64a的位置
在这里插入图片描述
这样elf的生成和加载的过程就全部已经完成了。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值