

鸿蒙内核源码分析(进程镜像篇) | ELF是如何被加载运行的? | 百篇博客分析HarmonyOS源...
source link: https://my.oschina.net/weharmony/blog/5060359
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

将 HarmonyOS | 鸿蒙 研究到底 < 国内 | 国外 >
百篇博客系列篇.本篇为:
可执行文件和共享目标文件(动态连接库)是程序的静态存储形式.要执行一个程序,系统要先把相应的可执行文件和动态连接库装载到进程空间中,这样形成一个可运行的进程的内存空间布局,也可以称它为"进程镜像".
本篇结合源码介绍鸿蒙加载和运行shell进程的整个过程,因本篇涉及代码较多,所以删减了一些不相干的代码. 鸿蒙加载和运行ELF的函数为 LOS_DoExecveFile
LOS_DoExecveFile
根文件系统已经提供shell,fileName为 "/bin/shell"
//运行用户态进程 ELF格式,运行在内核态
INT32 LOS_DoExecveFile(const CHAR *fileName, CHAR * const *argv, CHAR * const *envp)
{
ELFLoadInfo loadInfo = { 0 };
CHAR kfileName[PATH_MAX + 1] = { 0 };//此时已陷入内核态,所以局部变量都在内核空间
INT32 ret;
loadInfo.newSpace = OsCreateUserVmSapce();//创建用户虚拟空间
if (loadInfo.newSpace == NULL) {
PRINT_ERR("%s %d, failed to allocate new vm space\n", __FUNCTION__, __LINE__);
return -ENOMEM;
}
loadInfo.argv = argv;//参数数组
loadInfo.envp = envp;//环境数组
ret = OsLoadELFFile(&loadInfo);//加载ELF文件
if (ret != LOS_OK) {
return ret;
}
//对当前进程旧虚拟空间和文件进行回收
ret = OsExecRecycleAndInit(OsCurrProcessGet(), loadInfo.fileName, loadInfo.oldSpace, loadInfo.oldFiles);
if (ret != LOS_OK) {
(VOID)LOS_VmSpaceFree(loadInfo.oldSpace);//释放虚拟空间
goto OUT;
}
ret = OsExecve(&loadInfo);//运行ELF内容
if (ret != LOS_OK) {
goto OUT;
}
return loadInfo.stackTop;
OUT:
(VOID)LOS_Exit(OS_PRO_EXIT_OK);
return ret;
}
解读
- 创建了一个新的用户进程空间,每个应用进程都有自己独立的进程空间,也称虚拟空间.这个空间和内核空间是隔离的,用户空间的虚拟地址范围为 0x00000000 ~ 0x3FFFFFFF,内核空间是0x3FFFFFFF ~ 0xFFFFFFFF
- 加载ELF文件,注意 SysExecve -> LOS_DoExecveFile,而
SysExecve
是个系统调用,所以LOS_DoExecveFile
是运行在内核空间.加载过程由内核完成,包括申请的动态内存都是由内核空间提供. - 加载成功后,当前进程会被腾龙换鸟,把原有内脏挖空后留给新的
shell
使用,原用进程空间和文件都会被保存下来. - 运行shell,代码段,数据段装载完成后,设置好运行栈,运行就变得很简单,将用户栈保存到内核栈中,程序就会切到shell入口地址
0x1000
执行,正式开始了 shell 之旅
解析,装载过程
ELF一体两面,面对不同的场景扮演不同的角色,这是理解ELF的关键,链接器只关注1,3(区),4 的内容,加载器只关注1,2,3(段)的内容,本篇说明加载过程,所以不会出现区(sections)这个概念. 先看shell
1,2,3(段)的内容,这些内容看过
的不会陌生,对照着代码去看很容易理解.
root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# readelf -h shell
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: ARM
Version: 0x1
Entry point address: 0x1000
Start of program headers: 52 (bytes into file)
Start of section headers: 25268 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 11
Size of section headers: 40 (bytes)
Number of section headers: 27
Section header string table index: 26
root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# readelf -l shell
Elf file type is DYN (Shared object file)
Entry point 0x1000
There are 11 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00160 0x00160 R 0x4
INTERP 0x000194 0x00000194 0x00000194 0x00016 0x00016 R 0x1
[Requesting program interpreter: /lib/ld-musl-arm.so.1]
LOAD 0x000000 0x00000000 0x00000000 0x00e64 0x00e64 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x03690 0x03690 R E 0x1000
LOAD 0x005000 0x00005000 0x00005000 0x001b8 0x001b8 RW 0x1000
LOAD 0x006000 0x00006000 0x00006000 0x00034 0x00060 RW 0x1000
DYNAMIC 0x005008 0x00005008 0x00005008 0x000c8 0x000c8 RW 0x4
GNU_RELRO 0x005000 0x00005000 0x00005000 0x001b8 0x01000 R 0x1
GNU_EH_FRAME 0x000e54 0x00000e54 0x00000e54 0x0000c 0x0000c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0
EXIDX 0x000928 0x00000928 0x00000928 0x00010 0x00010 R 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .dynsym .gnu.hash .hash .dynstr .rel.dyn .ARM.exidx .rel.plt .rodata .eh_frame_hdr .eh_frame
03 .text .init .fini .plt
04 .init_array .fini_array .dynamic .got .got.plt
05 .data .bss
06 .dynamic
07 .init_array .fini_array .dynamic .got .got.plt .bss.rel.ro
08 .eh_frame_hdr
09
10 .ARM.exidx
ELFLoadInfo
理解ELFLoadInfo
是理解鸿蒙加载ELF运行的关键.代码都已经注释.
typedef struct {//加载ELF信息结构体
ELFInfo execInfo; //可执行文件信息
ELFInfo interpInfo;//解析器文件信息 lib/libc.so
const CHAR *fileName;//文件名称
CHAR *execName;//程序名称
INT32 argc; //参数个数
INT32 envc; //环境变量个数
CHAR *const *argv; //参数数组
CHAR *const *envp; //环境变量数组
UINTPTR stackTop;//栈底位置,递减满栈下,stackTop是高地址位
UINTPTR stackTopMax;//栈最大上限
UINTPTR stackBase;//栈顶位置
UINTPTR stackParamBase;//栈参数空间,放置启动ELF时的外部参数,大小为 USER_PARAM_BYTE_MAX 4K
UINT32 stackSize;//栈大小
INT32 stackProt;//LD_PT_GNU_STACK栈的权限 ,例如(RW)
UINTPTR loadAddr; //加载地址
UINTPTR elfEntry; //装载点地址 即: _start 函数地址
UINTPTR topOfMem; //虚拟空间顶部位置,loadInfo->topOfMem = loadInfo->stackTopMax - sizeof(UINTPTR);
UINTPTR oldFiles; //旧空间的文件映像
LosVmSpace *newSpace;//新虚拟空间
LosVmSpace *oldSpace;//旧虚拟空间
#ifdef LOSCFG_ASLR
INT32 randomDevFD;
#endif
} ELFLoadInfo;
解读
- 一个程序要运行需要两个必不可少的硬性条件.
-
- 指令在哪里,由
elfEntry
,它是.text
的开始位置,直接在 elf头中可以读到.
- 指令在哪里,由
-
- 拿到指令后在哪里运行,即栈在哪里,
ELFLoadInfo
有7个变量在描述栈信息.足以说明栈的重要性.栈的构建对应的是ELF的GNU_STACK
段,权限必须是(R + W)
- 拿到指令后在哪里运行,即栈在哪里,
-
interpInfo
对应的是ELF的INTERP
段,不是所有的ELF都会有INTERP
段,如下:
这个段的意思就是需要加载动态链接库,INTERP 0x000194 0x00000194 0x00000194 0x00016 0x00016 R 0x1 [Requesting program interpreter: /lib/ld-musl-arm.so.1]
/lib/ld-musl-arm.so.1
是libc.so
的一个软链,具体位置在根文件系统 /rootfs/lib/libc.so 位置.argv
,envc
命令行参数和环境变量内核会专门开辟4K空间,保存在栈底位置,一起保存的还有ELF的辅助向量表auxVector
.loadAddr
通过LOS_MMap
将各LOAD
段加载到对应的位置,并做好的虚拟地址和物理地址的映射关系保存在了映射区.
加载过程(OsLoadELFFile)
源码位置: ..\kernel\extended\dynload\src\los_load_elf.c
//加载ELF格式文件
INT32 OsLoadELFFile(ELFLoadInfo *loadInfo)
{
INT32 ret;
OsLoadInit(loadInfo);//初始化加载信息
ret = OsReadEhdr(loadInfo->fileName, &loadInfo->execInfo, TRUE);//读ELF头信息
if (ret != LOS_OK) {
goto OUT;
}
ret = OsReadPhdrs(&loadInfo->execInfo, TRUE);//读ELF程序头信息,构建进程映像所需信息.
if (ret != LOS_OK) {
goto OUT;
}
ret = OsReadInterpInfo(loadInfo);//读取段 INTERP 解析器信息
if (ret != LOS_OK) {
goto OUT;
}
ret = OsSetArgParams(loadInfo, loadInfo->argv, loadInfo->envp);//设置外部参数内容
if (ret != LOS_OK) {
goto OUT;
}
OsFlushAspace(loadInfo);//擦除空间
ret = OsLoadELFSegment(loadInfo);//加载段信息
if (ret != LOS_OK) {//加载失败时
OsCurrProcessGet()->vmSpace = loadInfo->oldSpace;//切回原有虚拟空间
LOS_ArchMmuContextSwitch(&OsCurrProcessGet()->vmSpace->archMmu);//切回原有MMU
goto OUT;
}
OsDeInitLoadInfo(loadInfo);//ELF和.so 加载完成后释放内存
return LOS_OK;
OUT:
OsDeInitFiles(loadInfo);
(VOID)LOS_VmSpaceFree(loadInfo->newSpace);
(VOID)OsDeInitLoadInfo(loadInfo);
return ret;
}
解读
-
OsReadPhdrs
读取程序头(段头),共11个段头. -
OsReadInterpInfo
读取动态链接库lib/libc.so
段头信息. -
OsSetArgParams
将外部参数(命令行和环境变量)保存在栈底位置 -
OsFlushAspace
切换进程空间,新进程空间重置堆区,映射区,MMU切换.映射区一旦变化意味着MMU的L1,L2表的变化. -
OsLoadELFSegment
加载ELF.bss,.data,.text
区,这些区统一叫LOAD
段,建立新的虚拟地址和物理地址映射关系LOAD 0x000000 0x00000000 0x00000000 0x00e64 0x00e64 R 0x1000 LOAD 0x001000 0x00001000 0x00001000 0x03690 0x03690 R E 0x1000 LOAD 0x005000 0x00005000 0x00005000 0x001b8 0x001b8 RW 0x1000 LOAD 0x006000 0x00006000 0x00006000 0x00034 0x00060 RW 0x1000 四个加载段的内容对应以下各区,这些区都会加载到用户空间指定位置. 02 .interp .dynsym .gnu.hash .hash .dynstr .rel.dyn .ARM.exidx .rel.plt .rodata .eh_frame_hdr .eh_frame 03 .text .init .fini .plt 04 .init_array .fini_array .dynamic .got .got.plt 05 .data .bss
-
经过以上操作, shell在虚拟内存中真实样子如下:
但注意:其中不包含 /lib/libc.so的信息,动态链接部分会单独一篇去说明.
- 用户地址空间在 mmap处 一切为二, 堆区独占1/4, 所有区(.bbs,.text,..)共占1/4,映射区和栈区共占1/2,二者相立而行,向中间靠拢.
由 ..\kernel\extended\dynload\src\los_exec_elf.c 提供,很简单.
//运行ELF
STATIC INT32 OsExecve(const ELFLoadInfo *loadInfo)
{
if ((loadInfo == NULL) || (loadInfo->elfEntry == 0)) {
return LOS_NOK;
}
//任务运行的两个硬性要求:1.提供入口指令 2.运行栈空间.
return OsExecStart((TSK_ENTRY_FUNC)(loadInfo->elfEntry), (UINTPTR)loadInfo->stackTop,
loadInfo->stackBase, loadInfo->stackSize);
}
//执行用户态任务, entry为入口函数 ,其中 创建好task,task上下文 等待调度真正执行, sp:栈指针 mapBase:栈底 mapSize:栈大小
LITE_OS_SEC_TEXT UINT32 OsExecStart(const TSK_ENTRY_FUNC entry, UINTPTR sp, UINTPTR mapBase, UINT32 mapSize)
{
UINT32 intSave;
if (entry == NULL) {
return LOS_NOK;
}
if ((sp == 0) || (LOS_Align(sp, LOSCFG_STACK_POINT_ALIGN_SIZE) != sp)) {//对齐
return LOS_NOK;
}
//注意 sp此时指向栈底,栈底地址要大于栈顶
if ((mapBase == 0) || (mapSize == 0) || (sp <= mapBase) || (sp > (mapBase + mapSize))) {//参数检查
return LOS_NOK;
}
LosTaskCB *taskCB = OsCurrTaskGet();//获取当前任务
SCHEDULER_LOCK(intSave);//拿自旋锁
taskCB->userMapBase = mapBase;//用户态栈顶位置
taskCB->userMapSize = mapSize;//用户态栈
taskCB->taskEntry = (TSK_ENTRY_FUNC)entry;//任务的入口函数
//初始化内核态栈
TaskContext *taskContext = (TaskContext *)OsTaskStackInit(taskCB->taskID, taskCB->stackSize,
(VOID *)taskCB->topOfStack, FALSE);
OsUserTaskStackInit(taskContext, (UINTPTR)taskCB->taskEntry, sp);//初始化用户栈,将内核栈中上下文的 context->R[0] = sp ,context->sp = sp
//这样做的目的是将用户栈SP保存到内核栈中,
SCHEDULER_UNLOCK(intSave);//解锁
return LOS_OK;
}
解读
- 运行shell出奇的简单,设置好执行指令的入口地址(PC)寄出器和栈指针(SP)就可以了,这些内容在系列篇中已经反复说过,请自行翻看.
- 因shell为用户态进程,所以会有内核态和用户态两个栈,初始化内核栈
OsTaskStackInit
和用户栈OsUserTaskStackInit
过程在线程概念篇中也已有描述.
百篇博客.往期回顾
在加注过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆.说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,.xx
代表修改的次数,精雕细琢,言简意赅,力求打造精品内容.
关于 51 .c .h .o
看系列篇文章会常看到 51 .c .h .o
,希望这对大家阅读不会造成影响. 分别对应以下四个站点的首个字符,感谢这些站点一直以来对系列篇的支持和推荐,尤其是 oschina gitee ,很喜欢它的界面风格,简洁大方,让人感觉到开源的伟大!
而巧合的是.c .h .o
是C语言的头/源/目标文件,这就很有意思了,冥冥之中似有天数,将这四个宝贝以这种方式融合在一起. 51 .c .h .o
, 我要CHO ,嗯嗯,hin 顺口 : )
百万汉字注解.百篇博客分析
百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding >
百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< 51cto | csdn | harmony | osc >
关注不迷路.代码即人生
原创不易,欢迎转载,但麻烦请注明出处.
Recommend
-
11
kernel_liteos_a_note: 鸿蒙内核源码注释中文版 点击目录和文件查看详细源码中文注解,走进内核的世界.
-
12
百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee
-
10
百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee
-
25
百万汉...
-
6
百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee
-
13
fork是如何做到调用一次,返回两次的 ?百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新<
-
14
百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee
-
9
鸿蒙内核源码分析(信号生产篇) | 信号安装和发送过程是怎样的? | 百篇博客分析HarmonyOS源码 | v48.03 - 鸿蒙内核源码分析的个人空间 - OSCHINA - 中文开源技术交流社区
-
6
鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙看这篇或许真的够了 | 百篇博客分析HarmonyOS源码 | v50.02 - 鸿蒙内核源码分析的个人空间 - OSCHINA - 中文开源技术交流社区
-
10
百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新<
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK