鸿蒙内核源码分析(进程镜像篇)|ELF是如何被加载运行的? | 百篇博客分析OpenHarmony源码 | v56.02

702 阅读13分钟

子畏于匡,颜渊后。子曰:“吾以女为死矣。”曰:“子在,回何敢死?” 《论语》:先进篇

在这里插入图片描述

百篇博客系列篇.本篇为:

v56.xx 鸿蒙内核源码分析(进程映像篇) | ELF是如何被加载运行的?

加载运行相关篇为:

可执行文件和共享目标文件(动态连接库)是程序的静态存储形式.要执行一个程序,系统要先把相应的可执行文件和动态连接库装载到进程空间中,这样形成一个可运行的进程的内存空间布局,也可以称它为"进程映像".

本篇结合源码介绍鸿蒙加载和运行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(ELF头信息),3(区),4(区头表) 的内容,加载器只关注1(ELF头信息),2(段头表),3(段)的内容,本篇说加载过程,所以不会出现区(sections)这个概念. 先看shell 1,2,3(段)的内容,这些内容看过

  • [v53.xx 鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 ]
  • [v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main ]

的不会陌生,对照着代码去看很容易理解.

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  

解读

  • 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;

解读

  • 一个程序要运行需要两个必不可少的硬性条件.
      1. 指令在哪里,由 elfEntry,它是.text的开始位置,直接在 elf头中可以读到.
      1. 拿到指令后在哪里运行,即栈在哪里,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.1libc.so的一个软链,具体位置在根文件系统 /rootfs/lib/libc.so 位置.
  • argvenvc命令行参数和环境变量内核会专门开辟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;
}

解读

  • 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在虚拟内存中真实样子如下:
内存映像虚拟地址范围大小备注
stack 向下生长USER_ASPACE_TOP_MAX ~ USER_MAP_SIZE + USER_MAP_BASE
mmap 向上生长USER_MAP_SIZE + USER_MAP_BASE ~ USER_MAP_BASEUSER_MAP_SIZEUSER_MAP_BASE = (USER_ASPACE_TOP_MAX >> 1)
heap 向上生长USER_MAP_BASE ~ USER_HEAP_BASEUSER_HEAP_BASE = USER_ASPACE_TOP_MAX >> 2
.data .bss0x06060 ~ 0x0060000x00060
.init_array .fini_array .dynamic .got .got.plt0x051b8 ~ 0x0050000x001b8
.text .init .fini .plt0x04690 ~ 0x0010000x03690
.interp .dynsym .gnu.hash .hash .dynstr .rel.dyn .ARM.exidx .rel.plt .rodata .eh_frame_hdr .eh_frame0x00e64 ~ 0x0000000x00e64

但注意:其中不包含 /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过程在线程概念篇中也已有描述.

百万汉字注解.精读内核源码

四大码仓同步注解内核源码, >> 查看 gitee 仓库

百篇博客分析.深挖内核地基

给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 :P
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,.xx 代表修改的次数,精雕细琢,言简意赅,力求打造精品内容。

编译构建基础工具加载运行进程管理
编译环境
编译过程
环境脚本
构建工具
gn应用
忍者ninja
双向链表
位图管理
用栈方式
定时器
原子操作
时间管理
ELF格式
ELF解析
静态链接
重定位
进程映像
进程管理
进程概念
Fork
特殊进程
进程回收
信号生产
信号消费
Shell编辑
Shell解析
进程通讯内存管理前因后果任务管理
自旋锁
互斥锁
进程通讯
信号量
事件控制
消息队列
内存分配
内存管理
内存汇编
内存映射
内存规则
物理内存
总目录
调度故事
内存主奴
源码注释
源码结构
静态站点
时钟任务
任务调度
任务管理
调度队列
调度机制
线程概念
并发并行
系统调用
任务切换
文件系统硬件架构
文件概念
文件系统
索引节点
挂载目录
根文件系统
字符设备
VFS
文件句柄
管道文件
汇编基础
汇编传参
工作模式
寄存器
异常接管
汇编汇总
中断切换
中断概念
中断管理

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。