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

首先来看一下在 C语言程序是如何经过处理变成可执行程序的:


生成可执行程序的过程

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

目标文件的格式ELF
EXECUTABLE AND LINKABLE FORMAT 可执行的和可链接的格式(是文件格式的标准)
.o文件 和 可执行文件,都是目标文件,一般使用相同的文件格式
ABI和目标文件格式是什么关系
目标文件也叫做ABI,应用程序二进制接口。实际上在目标文件里面,它已经是二进制兼容的格式。而什么叫二进制兼容呢?所谓的二进制兼容,就是指这个目标文件已经是适应某一种CPU体系结构上的二进制的指令,比如在32位x86环境下编译出来的目标文件,链接成ARM上的可执行文件,那肯定是不可以的
ELF文件里面三种目标文件
一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其它的object文件一起来创建一个可执行文件或者是一个共享文件(主要是.o文件)
一个可执行(executable)文件保存着一个用来执行的程序,该文件指出了exec(BA_OS)如何来创建程序进程映象(操作系统怎么样把可执行文件加载起来并且从哪里开始执行)
一个共享object文件保存着代码和合适的数据,用来被下面两个链接器链接:(主要是.so文件)第一个是链接编辑器(静态链接)【请参看ld(SD_CMD)】,可以和其它的可重定位和共享object文件来创建其它的object第二个是动态链接器,联合一个可执行文件和其它的共享object文件来创建一个进程映象

ELF的目标文件格式

Object文件参与程序的链接(创建一个程序)和程序的执行(运行一个程序)

一个ELF头在文件的开始,保存了路线图(road map),描述了该文件的组织情况程序头表(Program header table)告诉系统如何来创建一个进程的内存映像Section头表(Section header table)包含了描述文件Sections的信息。每个Section在这个表中有一个入口,每个入口给出了该Section的名字,大小等信息

链接视图,有很多Section,执行视图,有很多段(Segment)

大多数文件格式也都是这种模式,在头记录了一些元数据,用readelf命令来详细查看ELF文件头

可执行程序加载的主要工作

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

可执行文件有一个头部,里面有一些关键信息,Entry point Address,入口地址,即程序的起点,0x8048300,后面有一些代码,数据
对于进程来讲,进程有一个进程地址空间,而对于32位x86体系结构来讲,进程有4G的进程地址空间(逻辑地址),3G以上的地址空间只能在内核态下访问,在用户态的时候,只能访问0到3G的地址空间。
ELF可执行文件加载到内存的位置 与 ELF可执行文件加载到内存中开始执行的第一行代码
ELF可执行文件默认加载到内存0x8048000这个位置,从这个位置开始加载。前面加载ELF可执行文件的头部信息,但因不同文件大小不同,程序的实际入口为:0x8048x00,图例为0x8048300,也就是说这个位置是程序的实际入口地址,即刚加载过可执行文件的进程(一个进程加载了新的可执行文件之后,开始执行的入口点),就是从这个地方开始执行
简略地来看,图例里的文件是ELF的静态链接文件。静态链接的时候,会将所有代码放在一个代码段,把所有的链接都链接好了,所以从0x8048300开始一行行代码执行,压栈出栈,把整个程序执行完而实际上如果需要用到共享库,需要动态链接的话,会有多个代码段,情况会更复杂(暂不研究)

装载可执行程序之前的工作最主要的是两大部分:

  1. 可执行程序的文件格式
  2. 可执行程序的执行环境

一般是通过shell程序启动一个可执行程序,shell程序具体做了什么?而当启动加载一个可执行程序的时候,也就是发起一个系统调用execve,shell环境准备了哪些执行的上下文环境(用户态的执行环境)
再看看execve系统调用怎么样把一个可执行文件在内核态里面装载起来,装载起来后又返回到用户态(内核态的执行环境)


可执行程序的执行环境(Shell命令行、main函数的参数与execve的参数)
$ ls -l /usr/bin 列出/usr/bin下的目录信息,
Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身例如,
int main(int argc, char *argv[]) -- 愿意接收命令行参数又如,int main(int argc, char *argv[], char *envp[]) -- 愿意接收shell的环境变量.
前两个参数由用户输入命令的时候设定-l /usr/bin,后一个是shell环境,shell程序自动加上

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

先函数调用参数传递,再系统调用参数传递

Shell程序 -> execve -> sys_execve,然后在初始化新程序堆栈时拷贝进去

命令行参数和环境串都放在用户态堆栈中

当fork一个子进程的时候,复制父进程,调用execve系统调用的时候,要加载的可执行程序把原来的进程的环境覆盖掉了,覆盖掉之后它的用户态堆栈也被清空了,因为它是个新的程序要执行,那么argv和envp是如何进入新程序的用户态堆栈的?即命令行参数和环境变量是如何进入新程序的堆栈的?
在创建一个新的用户态堆栈的时候,实际上是把命令行参数的内容和环境变量的内容通过指针的方式传递到execve系统调用的内核处理函数,然后内核处理函数在创建可执行程序新的用户态堆栈的时候,会把参数拷贝到用户态堆栈里,初始化新的可执行程序的上下文环境。所以,新的程序能从main函数开始,把对应的参数接收过来,然后执行。但原先在调用execve时,参数只是压在了shell程序当前进程的堆栈上,而这个堆栈在加载完新的可执行程序之后,已经被清空了,内核又创建了一个新进程的用户态堆栈

如果仅仅只是加载一个静态链接的可执行程序的话,只需要传递一些命令行参数,一些环境变量,可执行程序就可以正常地工作。但是对于绝大多数的可执行程序来讲,还有一些对动态链接库的依赖,这个比较复杂。
传统的系统调用都是陷入到内核态,然后再返回到用户态,继续执行系统调用下面的指令

fork系统调用进入到内核态,两次返回,在父进程中,返回到父进程原来的位置继续向下执行,这个和传统的系统调用是一样的。在子进程中,构造了它的堆栈环境,子进程返回到特定的点,是从ret_from_fork开始执行然后返回到用户态,对于子进程来讲比较特殊

execve系统调用,当前的可执行程序在执行,执行到execve系统调用时候,陷入到内核态,在内核里面,用execve加载的可执行文件,把当前进程的可执行程序给覆盖掉了,当execve系统调用返回的时候,已经不是返回到原来的可执行程序了,是新的可执行程序的执行起点,也就是main函数大致的位置,那么main函数的执行环境,也就需要我们来构建好加载的新的可执行程序的执行环境

sys_execve内核处理过程

当execve系统调用陷入到内核里的时候,system_call,调用了sys_execve(),sys_execve内部会解析可执行文件格式,后面的调用顺序:do_execve -> do_execve_common -> exec_binprm

search_binary_handler根据文件头部信息寻找对应的文件格式处理模块,如下:

查找处理函数

根据我们给出的文件名,加载了文件的头部,判断文件是什么格式,在列表中寻找能够解释ELF格式的内核模块

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

当ELF文件格式出现的时候,观察者就能自动执行load_elf_binary,但实际上是在retval = fmt->load_binary(bprm)执行,这个地方实际上是一种多态的机制,本质上是一种观察者模式
动态调试时中断在 load_elf_binary,如下所示:

load_elf_binary

在start_thread函数中,将中断返回后的 ip 和 sp 设置成了特定的新值,以便执行新的程序逻辑.

start_thread

调试时也会中断在该函数,并可以查看新的 ip 地址.

动态调用 start_thread

start_thread这个函数有一个pt_regs,一个new_ip,一个new_sp
pt_regs实际上就是内核堆栈的栈底的那部分,发生系统调用int 0x80的时候,把eflags、sp、ip都压入到栈。那么新进程执行的时候,需要把它的起点位置给它替换掉

new_ip是怎么来的呢?
看一下load_elf_binary in /linux-3.18.6/fs/binfmt_elf.c#571

975 start_thread(regs, elf_entry, bprm->p);

elf_entry,对于一个静态链接的可执行文件,就是可执行文件里的Entry point address,可执行文件头部定义的起点
在一个新的可执行程序返回到用户态之前,需要修改int 0x80压入内核堆栈的EIP,用新的可执行程序的起点来修改,但是对于动态链接的过程又更复杂一些,先理解静态链接的过程,大致是这样

看一下do_execve_common
/linux-3.18.6/fs/exec.c#do_execve_common
打开要加载的可执行文件,然后加载文件头部

1474 file = do_open_exec(filename);

创建结构体,bprm

1481 bprm->file = file;
1482 bprm->filename = bprm->interp = filename->name;

把环境变量和参数都copy到结构体里面

1505 retval = copy_strings(bprm->envc, envp, berm);
1509 retval = copy_strings(bprm->argc, argv, berm);

对可执行文件的处理过程

1513 retval = exec_binprm(berm);

看一下exec_binprm /linux-3.18.6/fs/exec.c#exec_binprm
寻找可执行文件的处理函数
1416 ret = search_binary_handler(bprm);
看一下search_binary_handler /linux-3.18.6/fs/exec.c#1352
寻找能够解释当前可执行文件的代码模块

如果该程序需要动态链接,则elf_interpreter指针不为空,并指向对应的 ld 文件.内核则加载此文件,由该文件进行动态链接,并最终跳入程序头文件中制定的入口点.如下图所示:

动态加载与静态加载的区别

所以后面在start_thread的时候就会有两种可能

975 start_thread(regs, elf_entry, bprm->p);

如果是一个静态链接文件的话,elf_entry就是指向Entry point address的位置0x8048x00如果是一个需要依赖动态链接库的话,需要ld链接器,elf_entry就是指向动态链接器的起点

浅析动态链接的可执行程序的装载
对于一般的可执行程序来讲,大多都是需要使用动态链接库,动态链接库最常见的就是libc,动态链接器ld它也是libc的一部分。那么在动态链接的过程,内核做了什么?
ELF格式里面要依赖其它的动态链接库,动态链接库某一个.so本身(它也是一个ELF格式的文件)还可能会依赖其它的动态链接库,因此实际上动态链接库的依赖关系会形成一个图。在解释每一个ELF格式文件的时候,看它依赖了哪些动态链接库,这样它就会加载
那么谁负责加载呢?
阅读内核代码的时候,可以看到,当这个文件需要用elf_interpreter的话,也就是说它需要依赖动态链接器来解释这个ELF文件,那么它就需要加载load_elf_interp,实际上是加载动态连接器ld,那么这时候Entry point address,也就是说在返回到用户态的时候,它返回的就不是这个可执行程序文件规定的起点,它返回的是动态连接器的程序入口,动态连接器负责解释当前的可执行文件,看它里面依赖哪些动态链接库,然后把那些动态链接库一个一个加载进来,加载进来之后再解释加载进来的动态链接库,看它这个动态链接库还依赖哪些文件,这样就有一个叫广度遍历的方法(即动态链接库的装载过程是一个图的遍历),把所有的动态链接库都装载起来,装载起来之后ld再负责把CPU的控制权移交给可执行程序头部规定的起点位置
那么从以上分析看出,动态链接的过程不是由内核来完成的,主要是由动态链接器来完成的,动态链接器是libc的一部分,是在用户态做的事情

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,487评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,621评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,611评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,659评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,509评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,380评论 1 274
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,799评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,443评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,739评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,781评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,554评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,400评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,811评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,043评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,330评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,775评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,978评论 2 337

推荐阅读更多精彩内容