2019-2020-1 20199302《Linux内核原理与分析》第八周作业

一、上课学习笔记

1、shell作用:①运行程序 ②重定向(输入/输出重定向) ③可编程(写脚本)

执行一个c程序时,如果切进另一个进程,会进入该进程而切不回原进程,所以需要为调用的进程创一个子进程。

2、fork()

getpid()自己进程的id, getppid()父亲的进程, fork()的返回值是自己的子进程。

二、课本与视频学习内容

1、ELF格式 头部+代码+数据

ELF文件加载到内存时:把代码段和数据加载到内存中,默认从ELF文件头部信息中的Entry point address开始加载(程序的实际入口,可执行文件加载到内存中开始执行的第一行代码)。一般静态链接会将所有代码放在一个代码段,而动态链接的进程会有多个代码段。
目标文件的格式ELF:
最开始用的目标文件格式是A.out,后来发展成COFF文件格式,现在常用PE(在windows中常用)和ELF(在linux中常用)
ABI:应用程序二进制文件,是二进制兼容文件(目标文件已经适应某一种CPU体系结构的二进制指令)
ABI和目标文件格式的关系:目标文件也叫ABI

2、ELF三种目标文件类型

(1)可重定位文件:保存代码和适当数据,用来和其他object文件一起创建可执行文件或者共享文件。(.o文件)
(2)可执行文件:保存用来执行的程序,该文件指出了exec(BA_OS)如何来创建程序进程映像。
(3)共享object文件:保存代码和合适的数据,用来被下面两个链接器链接,第一个是链接编辑器(静态链接时,可以和其他的重定位和共享object文件创建其他的object),第二个是动态链接器(联合一个可执行文件和其他的共享object文件创建一个进程映像)
object文件:程序的链接(用来创建一个程序):ELF头(保存了路线图,描述该文件的组织情况),程序头表(告诉系统如何来创建一个进程的内存映像),多个节,section头表:包含了描述文件section的信息。每个section在这个头表中有一个入口,每个入口给出了该section的名字和大小等信息。
程序的执行(用来执行一个程序):ELF头,程序头表,多个段,段头表。
当创建或者增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段。(代码段和数据段)

3、可执行程序是怎么得来的

以C语言代码为例,经过编译器的预处理(下图省略这一步)后编译(gcc)成汇编代码(.asm),再经过汇编器将之编译(gas)为目标代码(.o),之后再链接(ld)成一个可执行文件(.out),之后操作系统将它加载到内存中执行。
2019-2020-1 20199302《Linux内核原理与分析》第八周作业_第1张图片
以C语言实例进行说明:
2019-2020-1 20199302《Linux内核原理与分析》第八周作业_第2张图片
hello.static静态编译出的文件,将所有执行需要的依赖放入文件中,所以文件会比hello文件大很多。

4、装载可执行程序之前的工作

一般我们通过shell启动一个可执行程序,那么shell程序帮我们做了什么?
可执行程序的执行环境
(1)命令行参数和shell环境,一般我们执行一个程序的shell环境,我们的实验直接使用exevue系统调用。shell本身不限制命令行参数的个数,命令行参数个数受限于命令本身
例如

int main(int argc,char argv[])用户自己设置的参数
int main(int argc,char
argv[],char * envp[])//envp[]代表shell环境变量

shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

int execve(const char filename,char const argv[],char * const envp[])
库函数exec*都是execve的封装例程。

命令行参数和环境变量是如何保存和传递的?
命令行参数和环境串都放在用户态堆栈中,当创建一个子进程的时候,完全复制父进程的信息,在调用execve系统调用时,当前要加载的可执行程序的进城环境把原来的进程环境覆盖掉,覆盖掉之后,用户态堆栈也被清空,那么
命令行参数和环境变量时如何进入到新程序的堆栈的?**
shell程序->execve->sys->execve,通过指针的方式,在初始化新程序堆栈时拷贝进去。
先函数调用参数传递,再系统调用参数传递。

5、 sys_execve的内部处理过程

sys_execve->do_execve->do_execve_common->exec_binprm
SYSCALL_DEFINE(系统调用的入口)调用do_execve(getname(filename),argv,envp)
在do_execve(struct filename *filename,const char __user *const __user *__argv,struct char __user *const __user *__envp)用户态指针,在它内部调用do_execve_common(filename,argv,envp)
在do_execve_common中,有

file = do_open_exec(filename);//打开可执行文件
//之后加载可执行文件的头部
retval = exec_binprm(bprm)//对可执行文件的处理过程

在exec_binprm(struct linux_binprm *bprm)中,可执行文件的处理过程,其中,关键代码有

ret = search_binary_handler(bprm);//寻找可执行文件的处理函数,在这个函数值中,寻找可以解析这个可执行文件的代码模块。

if(elf_interpreter),如果需要动态链接的话(即需要依赖动态库的话),需要加载load_elf_interp动态装载器的起点(即ld的链接器)。

6、重定位

重定位是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程,也就是说在装入时对目标程序中指令和数据的修改过程。是实现多道程序在内存中同时运行的基础
重定位的步骤

重定位和符号定义:链接器将所有的相同类型的节合并为同一类型的新的聚合节,将运行时存储器地址赋给新的聚合节,、输出模块定义的每个节,以及输入模块定义的每个符号。
重定位节中的符号的引用:链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。
符号表记录了目标文件中所有全局函数以及其地址。
重定位表中记录了所有调用这些函数的代码位置。

7、静态链接与动态链接

(1)静态链接
在编译链接时直接将需要的执行执行代码复制到最终可执行文件中,编译时会把需要所有代码链接进去,应用程序相对比较大
优点:代码装载速度快,,执行速度快,对外部环境依赖度低。
缺点:如果多个应用程序使用同一库函数,会被装载多次,浪费内存。
(2)动态链接
编译时不直接复制可执行代码,而是通过记录一系列符号和参数,在程序中运行或者加载时将这些信息传递给操作系统。操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定代码时,去共享执行内存中已经加载的动态库去执行代码,最终达到运行时链接的目的。
优点:多个程序可以共享同一段代码,而不需要在磁盘上存储多个复制。
缺点:影响程序前期运行性能,对使用的库依赖性较高。
动态链接分为运行时的动态链接和装载时的动态链接

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

先切换到 LinuxKernel目录下

rm menu -rf //删除menu

git clone https://github.com/mengning/menu.git //下载克隆新的menu

cd menu //切换到menu目录下

mv test_exce.c test.c //把test_exce.c 改成 test.c ,这里是应为Makefile里面用的是test.c

vi test.c //查看一下test.c的内容

vi makefile //查看一下Makefile的内容

make rootfs

再打开一个新的终端

gdb

file ../linux-3.18.6/vmlinux

target remote:1234

设置断点

2 b sys_execve (可以先停在sys_exceve然后再设置其他断点)

2 b search_bianry_handler

2 b load_elf_binary

2 b start_thread
如图所示:
2019-2020-1 20199302《Linux内核原理与分析》第八周作业_第3张图片
2019-2020-1 20199302《Linux内核原理与分析》第八周作业_第4张图片
2019-2020-1 20199302《Linux内核原理与分析》第八周作业_第5张图片
2019-2020-1 20199302《Linux内核原理与分析》第八周作业_第6张图片

Linux系统加载可执行程序所需处理过程的理解
1. 新的可执行程序是从哪里开始执行的?

当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。

如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址。

如果这个新程序是动态链接的,那么此时还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。

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

3. 对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?
execve系统调用会调用sys_execve,然后sys_execve调用do_execve,然后do_execve调用do_execve_common,然后do_execve_common调用exec_binprm,在exec_binprm中:

对于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控制权还给新进程。

你可能感兴趣的:(2019-2020-1 20199302《Linux内核原理与分析》第八周作业)