2021-2022-1 20212820《Linux内核原理与分析》第八周作业

Linux 内核如何装载和启动一个可执行程序

理解编译链接的过程和 ELF 可执行文件格式:​

可执行文件的创建过程:

C代码(.c) - 经过编译器预处理,编译成汇编代码(.asm) - 汇编器,生成目标代码(.o) - 链接器,链接成可执行文件(.out) - OS将可执行文件加载到内存里执行。如图所示:

 1. 预处理: gcc -E -o hello.cpp hello.c -m32    预处理(文本文件),预处理负责把include的文件包含进来及宏替换等工作

2. 编译 : gcc -x cpp-output -S -o hello.s hello.cpp -m32,编译成汇编代码(文本文件)

3. 汇编:gcc -x assembler -c hello.s -o hello.o -m32    汇编成目标代码(ELF格式,二进制文件,有一些机器指令,只是还不能运行)

4. 链接:gcc -o hello hello.o -m32    链接成可执行文件(ELF格式,二进制文件)在hello可执行文件里面使用了共享库,会调用printf,libc库里的函数,gcc -o hello.static hello.o -m32 -static    静态链接,把执行所需要依赖的东西都放在程序内部

实例:

vi hello.c
gcc -E -o hello.cpp hello.c -m32 //预处理.c文件,预处理包括把include的文件包含进来以及宏替换等工作
 
vi hello.cpp
gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译
 
vi hello.s
gcc -x assembler -c hello.s -o hello.o -m32 //汇编
 
vi hello.o
gcc -o hello hello.o -m32 //链接
 
vi hello
gcc -o hello.static hello.o -m32 -static 

ELF文件格式如下图所示:

ELF三种主要的目标文件:

  1. 可重定位:保存代码和适当数据,用来和其他的object文件一起创建可执行/共享文件,主要是.o文件
  2. 可执行文件:指出了exec如何创建程序进程映像,怎么加载,从哪里开始执行
  3. 共享object文件:保存代码和适当数据,用来被下面的两个连接器链接。
  4. (1)连接editor,连接可重定位、共享object文件。即装载时链接。
  5. (2)动态链接器,联合可执行、其他共享object文件创建进程映像。即运行时链接。

使用 exec*库函数加载一个可执行文件

动态链接分为可执行程序装载时动态链接和运行时动态链接

装载时动态链接

gcc -shared shlibexample.c -o libshlibexample.so -m32

运行时动态链接

gcc -shared dllibexample.c -o libdllibexample.so -m32

主调程序:

gcc main.c -o main -L /path/to/your/dir-l shlibexample -ldl -m32

注意:

编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl

准备.so文件

#ifndef _SH_LIB_EXAMPLE_H_
#define _SH_LIB_EXAMPLE_H_
 
#define SUCCESS 0
#define FAILURE (-1)
 
#ifdef __cplusplus
extern "C" {
#endif
/*
* Shared Lib API Example
* input : none
* output    : none
* return    : SUCCESS(0)/FAILURE(-1)
*
*/
int SharedLibApi();//内容只有一个函数头定义
 
#ifdef __cplusplus
}
#endif
#endif /* _SH_LIB_EXAMPLE_H_ */
/*------------------------------------------------------*/
 
#include <stdio.h>
#include "shlibexample.h"
 
int SharedLibApi()
{
    printf("This is a shared libary!\n");
    return SUCCESS;
}/* _SH_LIB_EXAMPLE_C_ */

动态加载库

#ifndef _DL_LIB_EXAMPLE_H_
#define _DL_LIB_EXAMPLE_H_
#ifdef __cplusplus
extern "C" {
#endif
/*
* Dynamical Loading Lib API Example
* input    : none
* output   : none
* return   : SUCCESS(0)/FAILURE(-1)
*
*/
int DynamicalLoadingLibApi();
#ifdef __cplusplus
}
#endif
#endif /* _DL_LIB_EXAMPLE_H_ */
/*------------------------------------------------------*/
#include <stdio.h>
#include "dllibexample.h"
#define SUCCESS 0
#define FAILURE (-1)
/*
* Dynamical Loading Lib API Example
* input    : none
 * output   : none
 * return   : SUCCESS(0)/FAILURE(-1)
 *
*/
int DynamicalLoadingLibApi()
{
printf("This is a Dynamical Loading libary!\n");
return SUCCESS;
}

main.c

int main()
{
printf("This is a Main program!\n");
/* Use Shared Lib */
printf("Calling SharedLibApi() function of libshlibexample.so!\n");
SharedLibApi();//可以直接调用
/* Use Dynamical Loading Lib */
void * handle = dlopen("libdllibexample.so",RTLD_NOW);//先打开动态加载库
if(handle == NULL)
{
    printf("Open Lib libdllibexample.so Error:%s\n",dlerror());
    return   FAILURE;
}
int (*func)(void);
char * error;
func = dlsym(handle,"DynamicalLoadingLibApi");
if((error = dlerror()) != NULL)
{
    printf("DynamicalLoadingLibApi not found:%s\n",error);
    return   FAILURE;
}    
printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");
func();  
dlclose(handle);//与dlopen函数配合,用于卸载链接库       
return SUCCESS;
}

使用 gdb 跟踪分析一个 execve 系统调用内核处理函数 sys_execve ,验证您对 Linux 系统加载可执行程序所需处理过程的理解

实验过程:

1.删除原来的menu,并clone新的menu

 

2.查看test.c文件:增加新的exec系统调用(红色区域是修改部分)

3.修改Makefile的值

 4.make rootfs之后新的内核启动,测试exec的功能

 5.进行gdb跟踪调试

 先停在sys_execve处,再设置其它断点;按c一路运行下去直到断点sys_execve

此处可以看见进口是相同的

 struct pt_regs *regs就是内核堆栈栈底的部分,发生中断的时候,esp和ip都进行压栈。通过修改内核堆栈中EIP的值(也就是把压入栈中的值用new_ip替换)作为新程序的起点。

总结:

  • 对“Linux内核装载和启动一个可执行程序”的理解

新的可执行程序通过修改内核堆栈eip作为新程序的起点,从new_ip开始执行后start_thread把返回到用户态的位置从int 0x80的下一条指令变成新加载的可执行文件的入口位置。当执行到execve系统调用时,进入内核态,用execve()加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的执行起点(main函数),所以execve系统调用返回后新的可执行程序能顺利执行。execve系统调用返回时,如果是静态链接,elf_entry指向可执行文件规定的头部(main函数对应的位置0x8048***);如果需要依赖动态链接库,elf_entry指向动态链接器的起点。动态链接主要是由动态链接器ld来完成的。

  • 特别关注新的可执行程序是从哪里开始执行的? 

 当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址。如果这个新程序是动态链接的,那么此时还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。

  • 为什么 execve 系统调用返回后新的可执行程序能顺利执行?

新的可执行程序执行,需要以下: 
1. 它所需要的库函数。 
2. 属于它的进程空间:代码段,数据段,内核栈,用户栈等。 
3. 它所需要的运行参数。 
4. 它所需要的系统资源。 
如果满足以上4个条件,那么新的可执行程序就会处于可运行态,只要被调度到,就可以正常执行。我们一个一个看这几个条件能不能满足。 
条件1:如果新进程是静态链接的,那么库函数已经在可执行程序文件中,条件满足。如果是动态链接的,新进程的入口地址是动态链接器ld的起始地址,可以完成对所需库函数的加载,也能满足条件。 
条件2:execve系统调用通过大幅度修改执行上下文,将用户态堆栈清空,将老进程的进程空间替换为新进程的进程空间,新进程从老进程那里继承了所需要的进程空间,条件满足。 
条件3:我们一般在shell中,输入可执行程序所需要的参数,shell程序把这些参数用函数参数传递的方式传给给execve系统调用,然后execve系统调用以系统调用参数传递的方式传给sys_execve,最后sys_execve在初始化新程序的用户态堆栈时,将这些参数放在main函数取参数的位置上。条件满足。 
条件4:如果当前系统中没有所需要的资源,那么新进程会被挂起,直到资源有了,唤醒新进程,变为可运行态,条件可以满足。 
综上所述,新的可执行程序可以顺利执行。

  • 对于静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同?

execve系统调用会调用sys_execve,然后sys_execve调用do_execve,然后do_execve调用do_execve_common,然后do_execve_common调用exec_binprm,在exec_binprm中:

ret = search_binary_handler(bprm);//寻找符合文件格式的对应解析模块(如ELF)
...
一个循环:
    retval = fmt->load_binary(bprm);
...

 对于ELF文件格式,fmt函数指针实际会执行load_elf_binary,load_elf_binary会调用start_thread,在start_thread中通过修改内核堆栈中EIP的值,使其指向elf_entry,跳转到elf_entry执行。 
对于静态链接的可执行程序,elf_entry是新程序的执行起点。对于动态链接的可执行程序,需要先加载链接器ld, 
elf_entry = load_elf_interp(…) 
将CPU控制权交给ld来加载依赖库,再由ld在完成加载工作后将CPU控制权还给新进程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值