本篇关键词:、、、
下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf
编译运行相关篇为:
- v84.02 鸿蒙内核源码分析(编译过程) | 简单案例说透中间过程
- v85.03 鸿蒙内核源码分析(编译构建) | 编译鸿蒙防掉坑指南
- v86.04 鸿蒙内核源码分析(GN语法) | 如何构建鸿蒙系统
- v87.03 鸿蒙内核源码分析(忍者无敌) | 忍者的特点就是一个字
- v88.04 鸿蒙内核源码分析(ELF格式) | 应用程序入口并非main
- v89.03 鸿蒙内核源码分析(ELF解析) | 敢忘了她姐俩你就不是银
- v90.04 鸿蒙内核源码分析(静态链接) | 一个小项目看中间过程
- v91.04 鸿蒙内核源码分析(重定位) | 与国际接轨的对外发言人
- v92.01 鸿蒙内核源码分析(动态链接) | 正在制作中 ...
- v93.05 鸿蒙内核源码分析(进程映像) | 程序是如何被加载运行的
- v94.01 鸿蒙内核源码分析(应用启动) | 正在制作中 ...
- v95.06 鸿蒙内核源码分析(系统调用) | 开发者永远的口头禅
- v96.01 鸿蒙内核源码分析(VDSO) | 正在制作中 ...
可执行文件和共享目标文件(动态连接库)是程序的静态存储形式。要执行一个程序,系统要先把相应的可执行文件和动态连接库装载到进程空间中,这样形成一个可运行的进程的内存空间布局,也可以称它为"进程映像"。
本篇结合源码介绍鸿蒙加载和运行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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
解读
- 创建了一个新的用户进程空间,每个应用进程都有自己独立的进程空间,也称虚拟空间。这个空间和内核空间是隔离的,用户空间的虚拟地址范围为 0x00000000 ~ 0x3FFFFFFF,内核空间是0x3FFFFFFF ~ 0xFFFFFFFF
- 加载ELF文件,注意 SysExecve -> LOS_DoExecveFile,而
SysExecve
是个系统调用,所以LOS_DoExecveFile
是运行在内核空间。加载过程由内核完成,包括申请的动态内存都是由内核空间提供。 - 加载成功后,当前进程会被腾龙换鸟,把原有内脏挖空后留给新的
shell
使用,原用进程空间和文件都会被保存下来。 - 运行shell,代码段,数据段装载完成后,设置好运行栈,运行就变得很简单,将用户栈保存到内核栈中,程序就会切到shell入口地址
0x1000
执行,正式开始了 shell 之旅
如何加载?
ELF一体两面,面对不同的场景扮演不同的角色,这是理解ELF的关键,链接器只关注1(ELF头信息),3(区),4(区头表) 的内容,加载器只关注1(ELF头信息),2(段头表),3(段)的内容,本篇说加载过程,所以不会出现区(sections)这个概念。
先看shell
1,2,3(段)的内容,这些内容看过
- v53.xx 鸿蒙内核源码分析(ELF解析篇)
- v51.xx 鸿蒙内核源码分析(ELF格式篇)
的不会陌生,对照着代码去看很容易理解。
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
解读
- INTERP 段,说明ELF需要加载另一个动态链接库
/lib/ld-musl-arm.so.1
。 - GNU_STACK 段,指的就是栈,没有它内核无法构建栈,而且必须是RW
- LOAD 段,指加载段,即.bss,.data,.text都属于加载段,加载它们到指定位置就是加载器的工作,而ELF本身已经提供了指令/数据的相对位置。加载器只需提供一个加载开始地址就能计算出指令/数据在虚拟空间中的最终地址。
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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
解读
一个程序要运行需要两个必不可少的硬性条件。
- 指令在哪里,由
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]
1
2这个段的意思就是需要加载动态链接库,
/lib/ld-musl-arm.so.1
是libc.so
的一个软链,具体位置在根文件系统 /rootfs/lib/libc.so 位置。argv
,envc
命令行参数和环境变量内核会专门开辟4K空间,保存在栈底位置,一起保存的还有ELF的辅助向量表auxVector
。loadAddr
通过LOS_MMap
将各LOAD
段并做好的虚拟地址和物理地址的映射关系保存在了映射区。- 从代码看对
.bss
区做了匿名映射,见于OsSetBss()
,不清楚为何内核要区别对待.bss
区。 - 其余各区做了文件映射。
- 从代码看对
加载过程(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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
解读
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
1
2
3
4
5
6
7
8
9经过以上操作, shell在虚拟内存中真实样子如下:
| 内存映像 | 虚拟地址范围 | 大小 |备注| |stack 向下生长|USER_ASPACE_TOP_MAX ~ USER_MAP_SIZE + USER_MAP_BASE|| |mmap 向上生长 |USER_MAP_SIZE + USER_MAP_BASE ~ USER_MAP_BASE| USER_MAP_SIZE|USER_MAP_BASE = (USER_ASPACE_TOP_MAX >> 1) |heap 向上生长 |USER_MAP_BASE ~ USER_HEAP_BASE||USER_HEAP_BASE = USER_ASPACE_TOP_MAX >> 2 |.data .bss |0x06060 ~ 0x006000|0x00060| |.init_array .fini_array .dynamic .got .got.plt| 0x051b8 ~ 0x005000|0x001b8| |.text .init .fini .plt|0x04690 ~ 0x001000|0x03690| |.interp .dynsym .gnu.hash .hash .dynstr .rel.dyn .ARM.exidx .rel.plt .rodata .eh_frame_hdr .eh_frame|0x00e64 ~ 0x000000 |0x00e64|
1
2
3
4
5
6
7
8但注意:其中不包含 /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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
解读
- 运行shell出奇的简单,设置好执行指令的入口地址(PC)寄出器和栈指针(SP)就可以了,这些内容在系列篇中已经反复说过,请自行翻看。
- 因shell为用户态进程,所以会有内核态和用户态两个栈,初始化内核栈
OsTaskStackInit
和用户栈OsUserTaskStackInit
过程在线程概念篇中也已有描述。
百文说内核 | 抓住主脉络
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 - 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。
按功能模块:
百万注源码 | 处处扣细节
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
关注不迷路 | 代码即人生
- 互联网从业十五年,计算机硕士,技术副总裁
- 关注我,持续更新四十年,即聊技术也聊人生
- 交有趣靠谱的人;做难而正确的事
- 不做作,不炒作,只唯真
- 不唯上,不唯书,只唯实
据说喜欢 点赞 + 分享 的,后来都成了大神。😃