第七周 可执行程序的卸载

一.预处理,编译,链接和目标文件的格式

可执行程序是怎么得来的:

预处理负责把include的文件包含进来及宏替换等工作

可执行文件的创建——预处理、编译和链接:

目标文件的格式ELF:A.out-->COFF-->PE/ELF

ABI&目标文件格式:

一个可重定位保存着代码和适当的数据,用于和其他的object文件一起来创建一个可执行文件或是一个共享文件(主要是.o文件)

一个可执行文件保存着一个用来执行的程序,该文件指出exec(BA_OS)如何来创建程序进程映像(主要是.so文件)

当创建或增加一个进程映像的时候,系统理论上将拷贝一个文件的段到一个虚拟的内存段

一般静态链接会将所有代码放在一个代码段,动态链接的进程会有多个代码段

二.可执行程序,共享库和动态链接

可执行程序的执行环境

  • 命令行参数和shell环境,一般我们执行一个程序的Shell环境即execve系统调用。

    • $ ls -l /usr/bin 列出/usr/bin下的目录信息

    • Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

      • 例如,int main(int argc, char *argv[])

      • 又如, int main(int argc, char *argv[], char *envp[])

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

      • int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

      • 库函数exec*都是execve的封装例程

命令行参数和环境变量如何保存和传递:环境串都放在用户态堆栈中

三.可执行程序的卸载

  • 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。
      • Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

        • 例如,int main(int argc, char *argv[])

        • 又如, int main(int argc, char *argv[], char *envp[])

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

        • int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

        • 库函数exec*都是execve的封装例程

  • sys_execve内部会解析可执行文件格式
      • do_execve -> do_execve_common ->  exec_binprm
      • search_binary_handler符合寻找文件格式对应的解析模块

      • 对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary,其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读

四.实验

要求:使用gdb跟踪sys_execve内核函数的处理过程,分析exec*函数对应的系统调用处理过程,理解Linux内核如何装载和启动一个可执行程序。

shell终端,执行以下命令:

cd LinuxKernel

rm -rf menu

git clone https://github.com/mengning/menu.git

cd menu

mv test_exec.c test.c

make rootfs

可以看到启动后的MenuOS已经包含了exec命令

可以通过增加-s -S启动参数打开调试模式

qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S

打开gdb进行远程调试

gdb

file ../linux-3.18.6/vmlinux

target remote:1234

设置断点

b sys_execve

b do_execve

b do_execve_common

b exec_binprm

b search_binary_handler

b load_elf_binary

b start_thread

分析:

通过查看源码可以看出执行exec命令是通过调用execlp函数实现的,属于库函数exec*都是execve的封装例程。

装载和启动一个可执行程序依次调用以下函数:

sys_execve() -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()

总结:当linux内核或程序(例如shell)用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。

PS:程序编译链接过程

预处理:(.c -> .cpp)

gcc -E -o hello.cpp hello.c -m32

编译:(.cpp -> .s 汇编)

gcc -x cpp-output -S -o hello.s hello.cpp -m32

编译:(.s -> .o 二进制目标代码)

gcc -x assembler -c hello.s -o hello.o -m32

链接:(.o -> a.out)共享库

gcc -o hello hello.o -m32

静态编译:

gcc -o hello.static hello.o -m32 -static

c语言main函数格式:

int main(int argc, char *argv[])

int main(int argc, char *argv[], char *envp[])

库函数exec*都是execve的封装例程

execve函数格式:

int execve(const char * filename,char * const argv[ ],char * const envp[ ])

sys_execve -> do_execve -> do_execve_common ->  exec_binprm

说是exec系统调用,实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是:

int execl(const char *path, const char *arg, ...);

int execv(const char *path, char *const argv[]);

int execle(const char *path, const char *arg, ..., char * const envp[]);

int execve(const char *path, char *const argv[], char *const envp[]);

int execlp(const char *file, const char *arg, ...);

int execvp(const char *file, char *const argv[]);

其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

区别一:前4个取路径名作为参数,后2个取文件名作为参数;

区别二:参数表的传递(l表示list,v表示vector);

区别三:向新程序传递环境表。

动态链接:

装载时动态链接

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

原文地址:https://www.cnblogs.com/20135305yg/p/5353148.html