Linux内核分析作业第七周

岁酱吖の 提交于 2020-03-28 22:32:45

一、 预处理、编译、链接

gcc hello.c -o hello.
  • gcc编译源代码生成最终可执行的二进制程序,GCC后台隐含执行了四个阶段步骤。

    预处理 → 编译 → 汇编 → 链接
  • 预处理:编译器将C源代码中包含的头文件编译进来和执行宏替换等工作。

gcc -E hello.c -o hello.i

  编译:gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

gcc –S hello.i –o hello.s-S:该选项只进行编译而不进行汇编,生成汇编代码。

  汇编:把编译阶段生成的.s文件转成二进制目标代码.

gcc –c hello.s –o hello.o

  链接:将编译输出.o文件链接成最终的可执行文件。

gcc hello.o –o hello

  运行:若链接没有-o指明,则生成可执行文件默认为a.out

./hello

 

二、可执行文件

  1、在windows环境下,只要双击一个.exe的文件就可以执行一个程序,这个以.exe结尾的文件就是一个可执行文件。在andriod系统下,一个.apk的文件就是一个可执行文件,在linux系统下,可执行文件在linux环境下并没有什么特殊的后缀标记,只是在生成该文件时,它的属性设置了可执行(就是‘x’),那么他就是属于可执行文件。

  2、linux系统中,可执行文件的格式为elf(Executable and Linking Format)格式

    ELF文件有三种类型: 

  • 可重定位文件 :也就是通常称的目标文件,后缀为.o。链接器将它作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件。 
  • 共享文件 :这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。 
  • 可执行文件
    • 第一个是连接编辑器,可以和其他的可重定位和共享object文件来创建其他的object。
    • 第二个是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映象。
  3、查看一个可执行文件头部结构:
readelf -h
    可知ELF头是程序表

typedef struct {
Elf32_Word p_type; /* 段类型 */
Elf32_Off p_offset; /* 段位置相对于文件开始处的偏移量 */
Elf32_Addr p_vaddr; /* 段在内存中的地址 */
Elf32_Addr p_paddr; /* 段的物理地址 */
Elf32_Word p_filesz; /* 段在文件中的长度 */
Elf32_Word p_memsz; /* 段在内存中的长度 */
Elf32_Word p_flags; /* 段的标记 */
Elf32_Word p_align; /* 段在内存中对齐标记 */
}Elf32_Phdr;

4、可执行程序动态链接

(1)动态链接

load_elf_binary(...)
{
...
kernel_read();//其实就是文件解析
...
//映射到进程空间 0x804 8000地址
elf_map();//
...
if(elf_interpreter) //依赖动态库的话
{
...
//装载ld的起点 #获得动态连接器的程序起点
elf_entry=load_elf_interp(...);
...
}
else //静态链接
{
...
elf_entry = loc->elf_ex.e_entry;
...
}
...
//static exe: elf_entry: 0x804 8000
//exe with dyanmic lib: elf_entry: ld.so addr
start_thread(regs,elf_entry,bprm->p);
}

    • 实际上,装载过程是一个广度遍历,遍历的对象是“依赖树”。
    • 主要过程是动态链接器完成、用户态完成。

(2)装载时动态链接

/*准备.so文件*/
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example

/*编译成libshlibexample.so文件*/
$ gcc -shared shlibexample.c -o libshlibexample.so -m32

/*使用库文件(因为已经包含了头文件所以可以直接调用函数)*/
SharedLibApi();

  (3)运行时动态链接

dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example

/*编译成libdllibexample.so文件*/
$ gcc -shared dllibexample.c -o libdllibexample.so -m32

/*使用库文件*/
void * handle = dlopen("libdllibexample.so",RTLD_NOW);//先加载进来
int (*func)(void);//声明一个函数指针
func = dlsym(handle,"DynamicalLoadingLibApi");//根据名称找到函数指针
func(); //调用已声明函数

  (4)运行

$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
$ export LD_LIBRARY_PATH=$PWD
/*将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。*/

三、可执行程序的装载

1、sys_execve内核处理过程
  (1)新的可执行程序

  • 一般是地址空间为0x8048000或0x8048300

(2)execve与fork

  • execve和fork都是特殊一点的系统调用:一般的都是陷入到内核态再返回到用户态。
  • fork两次返回,第一次返回到父进程继续向下执行,第二次是子进程返回到ret_from_fork然后正常返回到用户
  • execve执行的时候陷入到内核态,用execve中加载的程序把当前正在执行的程序覆盖掉,当系统调用返回的时候也就返回到新的可执行程序起点。

  (3)execve

  •  执行到可执行程序 -> 陷入内核
  •  构造新的可执行文件 -> 覆盖掉原可执行程序
  • 返回到新的可执行程序,作为起点(也就是main函数)
  •  需要构造其执行环境;

Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数,先函数调用参数传递,再系统调用参数传递。
(4)静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时不同

  • 静态链接:elf_entry指向可执行文件的头部,一般是main函数,是新程序执行的起点。
  • 动态链接:elf_entry指向ld(动态链接器)的起点,加载load_elf_interp
四、实验

1、在实验楼虚拟机下,键入以下指令更新 MenuOS

cd LinuxKernel
rm menu -rf
git config --global user.name "Scott Chacon"
git config --global user.mail "schacon@gmail.com"
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs

2、MenuOS 系统完毕之后,在 MenuOS 中运行 help 指令与 exec 指令

3、以跟踪模式启动MenuOS
  关闭 MenuOS,以如下指令重启 MenuOS:

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

  水平分割终端模拟器窗口,运行gdb,让 MenuOS 完全启动,处于接收命令状态:

cd ~/LinuxKernel
gdb
(gdb) file linux-3.18.6/vmlinux
(gdb) target remote:1234
(gdb) c # 让 MenuOS 完全启动,处于接收命令状态

 按“Ctrl +C", 让 gdb 处于 gdb 命令行模式,设置断点
4、跟踪断点
在 MenuOS 命令行执行 exec 指令,gdb 自动捕捉到 1#断点
 
 
五、总结
  Linux 系统通过 execve API 启动一个新进程,该 API 又呼叫 sys_execve 系统调用,负责将新的程序代码和数据替换到新的进程中,打开可执行文件,载入依赖的库文件,申请新的内存空间,最后执行 start_thread,设置 new_ip、new_sp,完成新进程的代码和数据替换,然后返回,接下来就是执行新的进程代码了。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!