可执行程序的装载

谈愈敏 原创作品转载请注明出处 《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000

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

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

  • C代码
  • 编译器预处理
  • 编译成汇编代码
  • 汇编器编译成目标代码
  • 链接成可执行文件
  • 操作系统加载到内存执行

举例说明:

目标文件的格式ELF

常见的目标文件格式:

  • A.out -> COFF -> PE(windows)/ELF(linux)

目标文件也叫ABI:应用程序二进制接口

ABI是二进制兼容:目标文件已经适应某种CPU体系结构上的二进制指令

ELF格式中3种目标文件:

可执行文件头部:

静态链接的ELF可执行文件和进程的地址空间

ELF可执行文件加载到进程的地址空间时:

  • 默认加载起始地址是0x8048000
  • ELF头部大小不同,程序入口点也将不同,程序入口在头部有定义:Entry point address,即是可执行文件加载到内存中开始执行的第一行代码
  • 一般静态链接会将所有代码放在一个代码段
  • 动态链接的进程会有多个代码段,更复杂

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

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

了解可执行文件格式ELF,了解执行环境,一般通过shell程序启动可执行程序,提供执行的上下文环境,用户态环境和系统调用相结合。

可执行程序的执行环境

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

    • $ ls -l /usr/bin 列出/usr/bin下的目录信息 //ls是一个可执行程序
    • Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身//也就是main函数
    • 例如,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的封装例程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();//shell先创建子进程
if (pid<0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid==0)
{
/* child process */
execlp("/bin/ls","ls",NULL);//调用execlp加载可执行程序ls,这里是三个参数
}
else
{
/* parent process */
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!");
exit(0);
}
}

命令行参数和环境变量是如何保存和传递的?

创建的子进程完全复制父进程,调用execve时加载的可执行程序把子进程环境覆盖掉了,子进程用户态堆栈也被清空了,命令行参数和环境变量是如何保存在新的可执行程序的用户态堆栈呢?

参数传递过程:shell程序 -> execve系统调用 -> sys_execve 内核处理函数在初始化新程序堆栈时拷贝进去

先函数调用参数传递,再系统调用参数传递:调用execve时,参数压在shell程序当前进程的堆栈上,加载完新的可执行程序时被清空了,内核又创建了新的进程的用户态堆栈,把参数拷贝进去:

装载时动态链接和运行时动态链接应用举例

动态链接分为可执行程序装载时动态链接(用的较多)和运行时动态链接,如下代码演示了这两种动态链接。

1、准备.so文件

共享库:

shlibexample.h (1.3 KB) - Interface of Shared Lib Example

int SharedLibApi();  //定义了一个函数原型

shlibexample.c (1.2 KB) - Implement of Shared Lib Example

#include <stdio.h>
#include "shlibexample.h" //把shlibexample.h头文件加进来 /*
* Shared Lib API Example
* input : none
* output : none
* return : SUCCESS(0)/FAILURE(-1)
*
*/
int SharedLibApi()
{
printf("This is a shared libary!\n"); //打印这是一个共享库
return SUCCESS;
}

编译成libshlibexample.so文件

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

动态加载库:

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

int DynamicalLoadingLibApi();  //声明一个函数

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

#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;
}

编译成libdllibexample.so文件

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

2、分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件

main.c (1.9 KB) - Main program

#include <stdio.h>

#include "shlibexample.h" //只加了共享库

#include <dlfcn.h> //动态加载

/*
* Main program
* input : none
* output : none
* return : SUCCESS(0)/FAILURE(-1)
*
*/
int main()
{
printf("This is a Main program!\n");
/* Use Shared Lib */
printf("Calling SharedLibApi() function of libshlibexample.so!\n");
SharedLibApi(); //可直接调用,因为前面include "shlibexample.h"
/* Use Dynamical Loading Lib */
void * handle = dlopen("libdllibexample.so",RTLD_NOW); //使用dlopen加载起来
if(handle == NULL)
{
printf("Open Lib libdllibexample.so Error:%s\n",dlerror());
return FAILURE;
}
int (*func)(void); //定义一个函数指针
char * error;
func = dlsym(handle,"DynamicalLoadingLibApi"); //找到函数名DynamicalLoadingLibApi赋给func
if((error = dlerror()) != NULL)
{
printf("DynamicalLoadingLibApi not found:%s\n",error);
return FAILURE;
}
printf("Calling DynamicalLoadingLibApi() function of libdllibexample.so!\n");
func(); //使用func,即调用的是DynamicalLoadingLibApi
dlclose(handle);
return SUCCESS;
}

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

$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32 //库文件是系统默认给你找,dl:动态加载
$ export LD_LIBRARY_PATH=$PWD //将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
$ ./main //dllibexample的相关信息在main函数内部指明了,编译时并没有,下面运行结果有
This is a Main program!
Calling SharedLibApi() function of libshlibexample.so!
This is a shared libary!
Calling DynamicalLoadingLibApi() function of libdllibexample.so!
This is a Dynamical Loading libary! 注意这里:shlibexample共享库:装载可执行程序时完成动态链接
dllibexample动态加载共享库:在程序执行过程中有程序自身来装载共享库

三、可执行程序的装载

可执行程序的装载相关关键问题分析

特殊系统调用:

  • fork:两次返回,第一次父进程返回,子进程也返回,到特定的点:从ret_ from_fork开始执行然后返回用户态。
  • execve:陷入到内核态调用execve加载可执行程序,把当前的程序覆盖掉了,返回的是一个新的可执行程序。返回的是新程序的执行起点,一般是main函数,要构建好执行环境。

sys_execve内部会解析可执行文件格式:

do_ execve -> do_ execve_ common -> exec_binprm

search_ binary_handler寻找符合文件格式对应的解析模块,如下:被观察者模式:

1369    list_for_each_entry(fmt, &formats, lh) {// 在链表中寻找能够解析ELF格式的内核模块
1370 if (!try_module_get(fmt->module))
1371 continue;
1372 read_unlock(&binfmt_lock);
1373 bprm->recursion_depth++;
1374 retval = fmt->load_binary(bprm);//找到fmt这个链表结点,能够解析ELF格式,加载处理函数
1375 read_lock(&binfmt_lock);

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

Linux内核是如何支持多种不同的可执行文件格式的? 观察者模式:

82static struct linux_binfmt elf_format = {  //elf_format结构体变量
83 .module = THIS_MODULE,
84 .load_binary = load_elf_binary,//把load_elf_binary赋给了结构体变量中的函数指针
85 .load_shlib = load_elf_library,
86 .core_dump = elf_core_dump,
87 .min_coredump = ELF_EXEC_PAGESIZE,
88}; 2198static int __init init_elf_binfmt(void)
2199{
2200 register_binfmt(&elf_format); //把elf_format结构体变量注册进内核链表中
2201 return 0;
2202}

elf_ format 和 init_ elf_binfmt,这里就是观察者模式中的观察者。

execve系统调用返回到用户态从哪里开始执行?

load_ elf_ binary -> start_thread:

load_ elf_ binary中:start_thread(regs,elf_entry,bprm->p); //elf_entry就是可执行文件头部定义的起点

start_thread三个参数:pt_regs(内核堆栈的栈底) new_ip new_sp

发生中断int 0x80时把sp,ip都压入内核栈了,用新进程起点去替换以前的ip,就是new_ip,而传递进来的参数new_ip = elf_entry 正是可执行程序起点。

总结:通过修改内核堆栈中EIP的值作为新程序的起点。

sys_execve的内部处理过程

do_ execve(getname(filename),argv,envp) -> do_ execve_ common(filename,argv,envp) 中:

  • 1、do_ open_ exec 打开要加载的可执行文件
  • 2、创建结构体bprm,把参数copy进结构体
  • 3、exec_ binprm:对可执行文件的处理过程,关键代码search_ binary_ handler寻找可执行文件,这里面关键list_ for_ each_ entry(fmt, &formats, lh)在链表中寻找能够解析ELF格式的内核模块,fmt->load_ binary(bprm)找到fmt这个链表结点,指向能够解析ELF格式的模块,加载处理函数,执行的应该是load_ elf_binary,见上

load_ elf_binary:严格的解析ELF格式文件,核心工作:把ELF可执行文件映射到进程的地址空间(默认加载起始地址是0x8048000)

start_ thread(regs,elf_ entry,bprm->p):

  • 对于静态链接的文件elf_entry是新程序执行的起点(可执行文件头部定义的)
  • 需要动态链接的可执行文件先加载连接器ld,装载动态库elf_entry = load_elf_interp,指向动态链接器的起点。

实验

搭建环境:

cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
vi test.c //增加了exec
vi MakeFile
make rootfs

gdb调试

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

gdb
file ../linux-3.18.6/vmlinux
target remote:1234 b sys_execve
b load_elf_binary
b start_thread po new_ip : 0x8048d0a 返回到用户态的第一条指令的地址
readelf -h hello 入口点地址:0x8048d0a

总结

关于静态链接的可执行程序的装载:举一个简单的例子:shell装载hello,进入内核时是shell,返回就变成了一个新的进程hello,类似庄生梦蝶,这是对可执行程序装载的一个简单理解。静态链接时elf_entry直接指向可执行程序的入口。

关于动态链接的可执行程序的装载:ELF格式文件依赖动态链接库,动态链接库.so也可能依赖其他动态链接库,实际上动态链接库的依赖关系会形成一个图。需要动态链接的可执行文件先加载连接器ld,elf_entry指向动态链接器的起点,再看这个动态链接库是否还依赖与其他动态链接库,其实这整个过程是对一个图的遍历,把所有依赖的动态链接库都装载起来之后,ld将CPU的控制权交给可执行程序,主要由ld完成,不是内核。

20135220谈愈敏Blog7_可执行程序的装载的更多相关文章

  1. 20135220谈愈敏Blog8_进程的切换和系统的一般执行过程

    进程的切换和系统的一般执行过程 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-100002 ...

  2. 20135220谈愈敏Blog6_进程的描述和创建

    进程的描述和创建 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 进程 ...

  3. 20135220谈愈敏Blog2_操作系统是如何工作的

    操作系统是如何工作的 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 计 ...

  4. 20135220谈愈敏Blog3_构造一个简单的Linux系统MenuOS

    构造一个简单的Linux系统MenuOS 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1 ...

  5. 20135220谈愈敏Blog4_系统调用(上)

    系统调用(上) 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 用户态 ...

  6. 20135220谈愈敏Blog5_系统调用(下)

    系统调用(下) 谈愈敏 原创作品转载请注明出处 <Linux内核分析>MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 给Me ...

  7. 20135220谈愈敏Linux_总结

    Linux_总结 具体博客链接 计算机是如何工作的 操作系统是如何工作的 构造一个简单的Linux系统MenuOS 系统调用(上) 系统调用(下) 进程的描述和创建 可执行程序的装载 进程的切换和系统 ...

  8. 20135220谈愈敏Linux Book_3

    第3章 进程管理 进程是Unix操作系统抽象概念中最基本的一种,进程管理是操作系统的心脏所在. 3.1 进程 进程:处于执行期的程序以及相关的资源的总称. 线程:在进程中活动的对象,拥有独立的程序计数 ...

  9. 20135220谈愈敏Linux Book_17

    第17章 设备与模块 关于设备驱动和设备管理的四种内核成分: 设备类型:在所有 Unix 系统中为了统一普通设备的操作所采用的分类. 模块: Linux 内核中用于按需加载和卸载目标码的机制. 内核对 ...

随机推荐

  1. Windows Server 2012之搭建域控制器DC

    安装域控制器,域(Domain) 1,本地管理员权限 2,设置静态IP 地址 3,至少有一个NTFS分区 4,操作系统版本(web版除外)   设置静态IP地址    dcpromo.exe命令不生效 ...

  2. jQuery DataTables 行获取

    datatables的官方例子中似乎没有提到表格双击和获取相应行号的功能; 经过探索可以按照以下方式实现:  $("#example tbody tr").dblclick(fun ...

  3. 使用POI实现数据导出Excel表格

    package cn.sh.bzt.kwj.action; import java.io.IOException; import java.io.OutputStream; import java.t ...

  4. 修复 Java 内存模型,第 1 部分——Brian Goetz

    转自Java并发大师Brain Goetz:http://www.ibm.com/developerworks/cn/java/j-jtp02244/ (中文地址) http://www.ibm.co ...

  5. Tomcat如何添加管理员

    为Tomcat添加管理员,然后用管理员登录后就可以看到所有加载的工程包,以及运行的平台,还可以对项目进行管理,比如删和添加. 一.工具:apache-tomcat-7.0.39 二.添加步骤 1. 首 ...

  6. 计算机信息统计.vbs

    temp=0 set wshshell=wscript.createobject("wscript.shell") Set WshNetwork = WScript.Createo ...

  7. Azure 自动化里添加ResourceManager模块

    最近想尝试通过Azure里的自动化功能来控制VM的定时开关机,找到网上的一篇文章,  按照文章操作到"Import Azure Resource manager module"的第 ...

  8. libc.so.6被删后导致系统无法使用的原因及解决方法

    记一次升级glibc库发生的错误 今天给glibc库升级,发生了一件让我吓出一声汗的事情,我把动态库中的libc.so.6给删了,瞬间所有的非系统命令都无法使用,使用就报错 当时就吓尿了,生产环境被我 ...

  9. linux netstat 命令简解

    Netstat 简介: Netstat是在内核中访问网络及相关信息的程序,它能提供TCP连接,TCP和UDP监听,进程内存管理的相关报告.常见参数-a (all)显示所有选项,默认不显示LISTEN相 ...

  10. 探索 OpenStack 之(14):OpenStack 中 RabbitMQ 的使用

    本文是 OpenStack 中的 RabbitMQ 使用研究 两部分中的第一部分,将介绍 RabbitMQ 的基本概念,即 RabbitMQ 是什么.第二部分将介绍其在 OpenStack 中的使用. ...