
v51.04 鸿蒙内核源码分析(ELF格式) | 应用程序入口并非main 原创
颜渊死,子哭之恸。从者曰:“子恸矣。”曰:“有恸乎?非夫人之为恸而谁为!” 《论语》:先进篇
百篇博客系列篇.本篇为:
v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并非main
加载运行相关篇为:
- v51.04 鸿蒙内核源码分析(ELF格式) | 应用程序入口并非main
- v53.03 鸿蒙内核源码分析(ELF解析) | 敢忘了她姐俩你就不是银
- v54.04 鸿蒙内核源码分析(静态链接) | 一个小项目看中间过程
- v55.04 鸿蒙内核源码分析(重定位) | 与国际接轨的对外发言人
- v56.05 鸿蒙内核源码分析(进程映像) | 程序是如何被加载运行的
阅读之前的说明
先说明,本篇很长,也很枯燥,若不是绝对的技术偏执狂是看不下去的.将通过一段简单代码去跟踪编译成ELF格式后的内容.看看ELF
究竟长了怎样的一副花花肠子,用readelf
命令去窥视ELF的全貌,最后用objdump
命令反汇编ELF
.找到了大家熟悉main
函数.
开始之前先说结论:
*
- ELF 分四块,其中三块是描述信息(也叫头信息),另一块是内容,放的是所有段/区的内容.
-
- ELF头定义全局性信息
-
- Segment(段)头,内容描述段的名字,开始位置,类型,偏移,大小及每段由哪些区组成.
-
- 内容区,ELF有两个重要概念
Segment
(段) 和Section
(区),段比区大,二者之间关系如下:
- 每个
Segment
可以包含多个Section
- 每个
Section
可以属于多个Segment
Segment
之间可以有重合的部分- 拿大家熟知的
.text
,.data
,.bss
举例,它们都叫区,但它们又属于LOAD
段.
- 内容区,ELF有两个重要概念
-
- Section(区)头,内容描述区的名字,开始位置,类型,偏移,大小等信息
- ELF一体两面,面对不同的场景扮演不同的角色,这是理解ELF的关键,链接器只关注1,3(区),4 的内容,加载器只关注1,2,3(段)的内容
- 鸿蒙对
EFL
的定义在kernel\extended\dynload\include\los_ld_elf_pri.h
文件中
示例代码
在windows目录E:\harmony\docker\case_code_100
下创建 main.c文件,如下:
因在
v50.xx (编译环境篇) | docker编译鸿蒙真的很香
篇中已做好了环境映射,所以文件会同时出现在docker中.编译生成ELF
->运行->readelf -h
查看app
头部信息.
名正才言顺
一下是关于ELF的所有中英名词对照.建议先仔细看一篇再看系列篇部分.
ELF历史
-
ELF(Executable and Linking Format),即"可执行可连接格式",最初由UNIX系统实验室(UNIX System Laboratories – USL)做为应用程序二进制接口(Application Binary Interface - ABI)的一部分而制定和发布.是鸿蒙的主要可执行文件格式.
-
ELF的最大特点在于它有比较广泛的适用性,通用的二进制接口定义使之可以平滑地移植到多种不同的操作环境上.这样,不需要为每一种操作系统都定义一套不同的接口,因此减少了软件的重复编码与编译,加强了软件的可移植性.
ELF整体布局
ELF规范中把ELF文件宽泛地称为"目标文件 (object file)",这与我们平时的理解不同.一般地,我们把经过编译但没有连接的文件(比如Unix/Linux上的.o文件)称为目标文件,而ELF文件仅指连接好的可执行文件;在ELF规范中,所有符合ELF格式规范的都称为ELF文件,也称为目标文件,这两个名字是相同的,而经过编译但没有连接的文件则称为"可重定位文件 (relocatable file)“或"待重定位文件 (relocatable file)”.本文采用与此规范相同的命名方式,所以当提到可重定位文件时,一般可以理解为惯常所说的目标文件;而提到目标文件时,即指各种类型的ELF文件.
ELF格式可以表达四种类型的二进制对象文件(object files):
- 可重定位文件(relocatable file),用于与其它目标文件进行连接以构建可执行文件或动态链接库.可重定位文件就是常说的目标文件,由源文件编译而成,但还没有连接成可执行文件.在UNIX系统下,一般有扩展名".o".之所以称其为"可重定位",是因为在这些文件中,如果引用到其它目标文件或库文件中定义的符号(变量或者函数)的话,只是给出一个名字,这里还并不知道这个符号在哪里,其具体的地址是什么.需要在连接的过程中,把对这些外部符号的引用重新定位到其真正定义的位置上,所以称目标文件为"可重定位"或者"待重定位"的.
- 可执行文件(executable file)包含代码和数据,是可以直接运行的程序.其代码和数据都有固定的地址 (或相对于基地址的偏移 ),系统可根据这些地址信息把程序加载到内存执行.
- 共享目标文件(shared object file),即动态连接库文件.它在以下两种情况下被使用:第一,在连接过程中与其它动态链接库或可重定位文件一起构建新的目标文件;第二,在可执行文件被加载的过程中,被动态链接到新的进程中,成为运行代码的一部分.包含了代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.l、libc.so.l、ld-linux.so.l)使用的.
- 核心转储文件(core dump file,就是core dump文件)
在不同阶段,我们可以用不同视角来理解ELF
文件,整体布局如下图所示:
从上图可见,ELF格式文件整体可分为四大部分:
ELF Header
: 在文件的开始,描述整个文件的组织.即readelf -h app
看到的内容Program Header Table
: 告诉系统如何创建进程映像.用来构造进程映像的目标文件必须具有程序头部表,可重定位文件可以不需要这个表.表描述所有段(Segment)信息,即readelf -l app
看到的前半部分内容.Segments
:段(Segment
)由若干区(Section
)组成.是从加载器角度来描述ELF
文件.加载器只关心ELF header
,Program header table
和Segment
这三部分内容。 在加载阶段可以忽略 section header table 来处理程序(所以很多加固手段删除了section header table
)Sections
: 是从链接器角度来描述ELF
文件. 链接器只关心ELF header
,Sections
以及Section header table
这三部分内容。在链接阶段,可以忽略program header table
来处理文件.Section Header Table
:描述区(Section
)信息的数组,每个元素对应一个区,通常包含在可重定位文件中,可执行文件中为可选(通常包含) 即readelf -S app
看到的内容- 从图中可以看出
Segment
:Section
(M:N)是多对多的包含关系.Segment
是由多个Section
组成,Section
也能属于多个段.
ELF头信息
ELF
头部信息对应鸿蒙源码结构体为 LDElf32Ehdr
, 各字段含义已一一注解,很容易理解.
解读
显示的信息,就是 ELF header 中描述的所有内容了。这个内容与结构体 LDElf32Ehdr
中的成员变量是一一对应的!
Size of this header: 64 (bytes)
也就是说:ELF header 部分的内容,一共是 64 个字节。64个字节码长啥样可以用命令od -Ax -t x1 -N 64 app
看,并对照结构体LDElf32Ehdr
来理解.
简单解释一下命令的几个选项:
这里留意这几个内容,下面会说明,先记住.
段(Segment)头信息
段(Segment)信息对应鸿蒙源码结构体为 LDElf32Phdr
,
解读
用readelf -l
查看app
段头部表内容,先看命令返回的前半部分:
数一下一共13个段,其实在ELF头信息也告诉了我们共13个段
仔细看下这些段的开始地址和大小,发现有些段是重叠的.那是因为一个区可以被多个段所拥有.例如:0x2db8
对应的 .init_array
区就被第四LOAD
和 GNU_RELRO
两段所共有.
PHDR
,此类型header元素描述了program header table自身的信息.从这里的内容看出,示例程序的program header table在文件中的偏移(Offset
)为0x40
,即64号字节处.该段映射到进程空间的虚拟地址(VirtAddr
)为0x40
.PhysAddr
暂时不用,其保持和VirtAddr
一致.该段占用的文件大小FileSiz
为0x2d8
.运行时占用进程空间内存大小MemSiz
也为0x2d8
.Flags
标记表示该段的读写权限,这里R
表示只读,Align
对齐为8,表明本段按8字节对齐.
INTERP
,此类型header元素描述了一个特殊内存段,该段内存记录了动态加载解析器的访问路径字符串.示例程序中,该段内存位于文件偏移0x318
处,即紧跟program header table.映射的进程虚拟地址空间地址为0x318
.文件长度和内存映射长度均为0x1c
,即28个字符,具体内容为/lib64/ld-linux-x86-64.so.2
.段属性为只读,并按字节对齐.
LOAD
,此类型header
元素描述了可加载到进程空间的代码区或数据区:
- 其第二段包含了代码区,文件内偏移为0x1000,文件大小为0x225,映射到进程地址0x001000处,属性为只读可执行(RE),段地址按0x1000(4K)边界对齐.
- 其第四段包含了数据区,文件内偏移为0x2db8,文件大小为0x260,映射到进程地址0x003db8处,属性为可读可写(RW),段地址也按0x1000(4K)边界对齐.
DYNAMIC
,此类型header
元素描述了动态加载段,其内部通常包含了一个名为.dynamic
的动态加载区.这也是一个数组,每个元素描述了与动态加载相关的各方面信息,将在系列篇(动态加载篇)中介绍.该段是从文件偏移0x2dc8
处开始,长度为0x1f0
,并映射到进程的0x3dc8
.可见该段和上一个段LOAD4 0x2db8
是有重叠的.
GNU_STACK
,可执行栈,即栈区,在加载段的过程中,当发现存在PT_GNU_STACK,也就是GNU_STACK segment 的存在,如果存在这个这个段的话,看这个段的 flags 是否有可执行权限,来设置对应的值.必须为RW方式.
再看命令返回内容的后半部分-段区映射关系
13个段和31个区的映射关系,右边其实不止31个区,是因为一个区可以共属于多个段,例如 .dynamic
,.interp
,.got
Segment:Section(M:N)是多对多的包含关系.Segment是由多个Section组成,Section也能属于多个段.这个很重要,说第二遍了.
INTERP
段只包含了.interp
区LOAD2
段包含.interp
、.plt
、.text
等区,.text
代码区位于这个段. 这个段是 'RE’属性,只读可执行的.LOAD4
包含.dynamic
、.data
、.bss
等区, 数据区位于这个段.这个段是 'RW’属性,可读可写..data
、.bss
都是数据区,有何区别呢?.data(ZI data)
它用来存放初始化了的(initailized)全局变量(global)和初始化了的静态变量(static)..bss(RW data )
它用来存放未初始化的(uninitailized)全局变量(global)和未初始化的静态变量.DYNAMIC
段包含.dynamic
区.
区表
区(section)头表信息对应鸿蒙源码结构体为 LDElf32Shdr
,
示例程序共生成31个区.其实在头文件中也已经告诉我们了
通过readelf -S
命令看看示例程序中 section header table的内容,如下所示.
String Table
在 ELF header 的最后 2 个字节是 0x1e 0x00,即30. 它对应结构体中的成员 elfShStrIndex
,意思是这个 ELF 文件中,字符串表是一个普通的 Section,在这个 Section 中,存储了 ELF 文件中使用到的所有的字符串。
我们使用readelf -x
读出下标30区的数据:
可以发现,这里其实是一堆字符串,这些字符串对应的就是各个区的名字.因此section header table中每个元素的Name字段其实是这个string table的索引.为节省空间而做的设计,再回头看看ELF header中的 elfShStrIndex
,
它的值正好就是30,指向了当前的string table.
符号表 Symbol Table
Section Header Table中,还有一类SYMTAB
(DYNSYM)区,该区叫符号表.符号表中的每个元素对应一个符号,记录了每个符号对应的实际数值信息,通常用在重定位过程中或问题定位过程中,进程执行阶段并不加载符号表.符号表对应鸿蒙源码结构体为 LDElf32Sym
.
//kernel\extended\dynload\include\los_ld_elf_pri.h
用readelf -s
读出示例程序中的符号表,如下所示
在最后位置找到了亲切的老朋友 main
和say_hello
main
函数符号对应的数值为0x1174
,其类型为FUNC
,大小为30字节,对应的代码区索引为16.
say_hello
函数符号对应数值为0x1149
,其类型为FUNC
,大小为43字节,对应的代码区索引同为16.
Section Header Table:
反汇编代码区
在理解了String Table
和Symbol Table
的作用后,通过objdump
反汇编来理解一下.text
代码区:
0x1149
0x1174
正是say_hello
,main
函数的入口地址.并看到了激动人心的指令
很佩服你还能看到这里,牛逼,牛逼! 看了这么久还记得开头的C代码的样子吗? 再看一遍 : )
但是!!! 晕,怎么还有but,西卡西…,上面请大家记住的还有一个地方没说到
它的地址并不是main函数位置0x1174
,是0x1060
!而且代码区的开始位置是0x1060
没错的.
难度main
不是入口地址? 那0x1060
上放的是何方神圣,再查符号表发现是
从反汇编堆中找到 _start
这才看到了0x1174
的main
函数.所以真正的说法是:
- 从内核动态加载的视角看,程序运行首个函数并不是
main
,而是_start
. - 但从应用程序开发者视角看,
main
就是启动函数.
百篇博客分析.深挖内核地基
- 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
- 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
按功能模块:
- 前因后果 >> 总目录 | 调度故事 | 内存主奴 | 源码注释 | 源码结构 | 静态站点 |
- 基础工具 >> 双向链表 | 位图管理 | 用栈方式 | 定时器 | 原子操作 | 时间管理 |
- 加载运行 >> ELF格式 | ELF解析 | 静态链接 | 重定位 | 进程映像 |
- 进程管理 >> 进程管理 | 进程概念 | Fork | 特殊进程 | 进程回收 | 信号生产 | 信号消费 | Shell编辑 | Shell解析 |
- 编译构建 >> 编译环境 | 编译过程 | 环境脚本 | 构建工具 | gn应用 | 忍者ninja |
- 进程通讯 >> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 |
- 内存管理 >> 内存分配 | 内存管理 | 内存汇编 | 内存映射 | 内存规则 | 物理内存 |
- 任务管理 >> 时钟任务 | 任务调度 | 任务管理 | 调度队列 | 调度机制 | 线程概念 | 并发并行 | CPU | 系统调用 | 任务切换 |
- 文件系统 >> 文件概念 | 文件系统 | 索引节点 | 挂载目录 | 根文件系统 | 字符设备 | VFS | 文件句柄 | 管道文件 |
- 硬件架构 >> 汇编基础 | 汇编传参 | 工作模式 | 寄存器 | 异常接管 | 汇编汇总 | 中断切换 | 中断概念 | 中断管理 |
百万汉字注解.精读内核源码
四大码仓中文注解 . 定期同步官方代码
鸿蒙研究站( weharmonyos ) | 每天死磕一点点,原创不易,欢迎转载,请注明出处。若能支持点赞则更佳,感谢每一份支持。
