首页 文章 精选 留言 我的

精选列表

搜索[HarmonyOS SDK],共10000篇文章
优秀的个人博客,低调大师

鸿蒙内核源码分析(ELF反汇编篇) | 程序的入口函数并不是main | 百篇博客分析HarmonyOS源码 | v51.03

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< osc | 51cto | csdn | harmony > 阅读之前的说明 先说明,本篇很长,也很枯燥,若不是绝对的技术偏执狂是看不下去的.将通过一段简单代码去跟踪编译成ELF格式后的内容.看看ELF究竟长了怎样的一副花花肠子,用readelf命令去窥视ELF的全貌,最后用objdump命令反汇编ELF.找到了大家熟悉main函数. 开始之前先说结论: ELF有两个重要概念 Segment(段) and Section(区),段比区大,是包含关系,一个段可由多区组成,一个区可被多段所共有. ELF 分四块,其中三块是描述信息(也叫头信息),另一块是内容,放的是所有区的内容. 第一块ELF头定义全局性信息 第二块Segment(段)头,内容描述段的开始和结束位置,每段包含了哪些区. 第三块内容区,放.text,.data,.bss所有区的内容,为了表述严谨,系列篇将统一说代码区,数据区,不再说代码段. 第四块Section(区)头,内容描述区的开始和结束位置. 鸿蒙对EFL的定义在 kernel\extended\dynload\include\los_ld_elf_pri.h文件中 示例代码 在windows环境 E:\harmony\docker\test4harmony下创建 main.c文件,如下: #include <stdio.h> void say_hello(char *who) { printf("hello, %s!\n", who); } char *my_name = "harmony os"; int main() { say_hello(my_name); return 0; } 因在 v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | osc > 篇中已做好了映射,所以文件会同时出现在docker中.编译生成ELF->运行->readelf -h查看app头部信息. root@5e3abe332c5a:/home/docker/test4harmony# ls main.c root@5e3abe332c5a:/home/docker/test4harmony# gcc -o app main.c root@5e3abe332c5a:/home/docker/test4harmony# ls app main.c root@5e3abe332c5a:/home/docker/test4harmony# ./app hello, harmony os! root@5e3abe332c5a:/home/docker/test4harmony# readelf -h app ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x1060 Start of program headers: 64 (bytes into file) Start of section headers: 14784 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 31 Section header string table index: 30 这里留意这几个内容,下面会说明,先记住. Entry point address: 0x1060 //代码区 .text 起始位置,即程序运行开始位置 Number of program headers: 13 //段数量 Number of section headers: 31 //区数量 Section header string table index: 30 //字符串数组索引,该区记录所有区名称 ELF历史 ELF的英文全称是The Executable and Linking Format,最初是由UNIX系统实验室开发、发布的ABI(Application Binary Interface)接口的一部分,也是Linux的主要可执行文件格式。 ELF整体布局 ELF格式可以表达四种类型的二进制对象文件(object files): 可重定位文件(relocatable file,就是大家平常见到的.o文件)包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 可执行文件(executable file, 例上述示例代码生成的app文件)包含代码和数据,是可以直接运行的程序。其代码和数据都有固定的地址 (或相对于基地址的偏移 ),系统可根据这些地址信息把程序加载到内存执行。 共享库文件(shared object file,就是.so文件,用来做动态链接)也称动态库文件,包含了代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.l、libc.so.l、ld-linux.so.l)使用的。 核心转储文件(core dump file,就是core dump文件) 可重定位文件用在编译和链接阶段;可执行文件用在程序运行阶段;共享库则同时用在编译链接和运行阶段。在不同阶段,我们可以用不同视角来理解ELF文件,整体布局如下图所示: 从上图可见,ELF格式文件整体可分为四大部分: ELF Header, ELF头部,定义全局性信息,即readelf -h app看到的内容 Program Header Table, 描述段(Segment)信息的数组,每个元素对应一个段;通常包含在可执行文件中,可重定文件中可选(通常不包含) 即readelf -l app看到的前半部分内容 Segment and Section,段(Segment)由若干区(Section)组成;段在运行时被加载到进程地址空间中,包含在可执行文件中;区是段的组成单元,包含在可执行文件和可重定位文件中 即readelf -l app后半部分内容,并readelf -x ** app查看区具体内容. Section Header Table,描述区(Section)信息的数组,每个元素对应一个区;通常包含在可重定位文件中,可执行文件中为可选(通常包含) 即readelf -S app看到的内容 从图中可以看出 Segment:Section(M:N)是多对多的包含关系.Segment是由多个Section组成,Section也能属于多个段.系列篇统一将Segment注解为段, Section注解为区 ELF头信息 ELF头部信息对应鸿蒙源码结构体为 LDElf32Ehdr, 各字段含义已一一注解,很容易理解. //kernel\extended\dynload\include\los_ld_elf_pri.h /* Elf header */ #define LD_EI_NIDENT 16 typedef struct { UINT8 elfIdent[LD_EI_NIDENT]; /* Magic number and other info *///含前16个字节,又可细分成class、data、version等字段,具体含义不用太关心,只需知道前4个字节点包含`ELF`关键字,这样可以判断当前文件是否是ELF格式; UINT16 elfType; /* Object file type *///表示具体ELF类型,可重定位文件/可执行文件/共享库文件,显然这里是一个可执行文件;e_machine表示执行的机器平台,这里是x86_64; UINT16 elfMachine; /* Architecture *///表示cpu架构 UINT32 elfVersion; /* Object file version *///表示文件版本号,这里的1表示初始版本号; UINT32 elfEntry; /* Entry point virtual address *///对应`Entry point address`,程序入口函数地址,通过进程虚拟地址空间地址(0x400440)表达; UINT32 elfPhoff; /* Program header table file offset *///对应`Start of program headers`,表示program header table在文件内的偏移位置,这里是从第64号字节(假设初始为0号字节)开始; UINT32 elfShoff; /* Section header table file offset *///对应`Start of section headers`,表示section header table在文件内的偏移位置,这里是从第4472号字节开始,靠近文件尾部; UINT32 elfFlags; /* Processor-specific flags *///表示与CPU处理器架构相关的信息,这里为零; UINT16 elfHeadSize; /* ELF header size in bytes *///对应`Size of this header`,表示本ELF header自身的长度,这里为64个字节,回看前面的e_phoff为64,说明ELF header后紧跟着program header table; UINT16 elfPhEntSize; /* Program header table entry size *///对应`Size of program headers`,表示program header table中每个元素的大小,这里为56个字节; UINT16 elfPhNum; /* Program header table entry count *///对应`Number of program headers`,表示program header table中元素个数,这里为9个; UINT16 elfShEntSize; /* Section header table entry size *///对应`Size of section headers`,表示section header table中每个元素的大小,这里为64个字节; UINT16 elfShNum; /* Section header table entry count *///对应`Number of section headers`,表示section header table中元素的个数,这里为30个; UINT16 elfShStrIndex; /* Section header string table index *///对应`Section header string table index`,表示描述各section字符名称的string table在section header table中的下标。 } LDElf32Ehdr; 结构体对应readelf -h app命令内容来理解,就不再贴出来了. 段(Segment)头信息 段(Segment)信息对应鸿蒙源码结构体为 LDElf32Phdr, //kernel\extended\dynload\include\los_ld_elf_pri.h /* Program Header */ typedef struct { UINT32 type; /* Segment type */ UINT32 offset; /* Segment file offset */ UINT32 vAddr; /* Segment virtual address */ UINT32 phyAddr; /* Segment physical address */ UINT32 fileSize; /* Segment size in file */ UINT32 memSize; /* Segment size in memory */ UINT32 flags; /* Segment flags */ UINT32 align; /* Segment alignment */ } LDElf32Phdr; 用readelf -l查看app段头部表 root@5e3abe332c5a:/home/docker/test4harmony# readelf -l app Elf file type is DYN (Shared object file) Entry point 0x1060 There are 13 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8 ......... 解读 先看命令返回的前半部分: Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8 INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000618 0x0000000000000618 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000225 0x0000000000000225 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000190 0x0000000000000190 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000260 0x0000000000000268 RW 0x1000 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 GNU_EH_FRAME 0x000000000000201c 0x000000000000201c 0x000000000000201c 0x000000000000004c 0x000000000000004c R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000248 0x0000000000000248 R 0x1 数一下一共13个段,其实在ELF头信息也告诉了我们共13个段,仔细看下这些段的开始地址和大小,发现有些段是重叠的.那是因为一个区可以被多个段所拥有.例如:0x2db8 对应的 .init_array区就被第四LOAD 和 GNU_RELRO两段所共有. Number of program headers: 13 //段数量 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,映射到进程地址0x001000处,代码区长度为0x618个字节,属性为只读可执行(RE),段地址按0x1000(4K)边界对齐; 第四段包含了数据区,文件内偏移为0x1000,映射到进程地址为0x1000处(按2M对齐移动),文件大小为0x230,内存大小为0x238(因为其内部包含了8字节的bss区,即未初始化数据区,该区内容不占文件空间,但在运行时需要为其分配空间并清零),属性为读写(RW),段地址也按0x1000(4K)边界对齐。 DYNAMIC,此类型header元素描述了动态加载段,其内部通常包含了一个名为.dynamic的动态加载区;这也是一个数组,每个元素描述了与动态加载相关的各方面信息,将在系列篇(动态加载篇)中介绍。该段是从文件偏移0x2dc8处开始,长度为0x1f0,并映射到进程的0x3dc8;可见该段和上一个段LOAD4 0x2db8是有重叠的。 再看命令返回内容的后半部分-段区映射关系 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.got .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .dynamic .got Segment:Section(M:N)是多对多的包含关系.Segment是由多个Section组成,Section也能属于多个段.这个很重要,说第二遍了. INTERP段只包含了.interp区 LOAD2段包含.interp、.plt、.text等区,.text代码区位于这个段. LOAD4包含.dynamic、.data、.bss等区,数据区位于这个段. DYNAMIC段包含.dynamic`区。 区节表 区节(section)信息对应鸿蒙源码结构体为 LDElf32Shdr, //kernel\extended\dynload\include\los_ld_elf_pri.h /* Section header */ typedef struct { UINT32 shName; /* Section name (string tbl index) *///表示每个区的名字 UINT32 shType; /* Section type *///表示每个区的功能 UINT32 shFlags; /* Section flags *///表示每个区的属性 UINT32 shAddr; /* Section virtual addr at execution *///表示每个区的进程映射地址 UINT32 shOffset; /* Section file offset *///表示文件内偏移 UINT32 shSize; /* Section size in bytes *///表示区的大小 UINT32 shLink; /* Link to another section *///Link和Info记录不同类型区的相关信息 UINT32 shInfo; /* Additional section information *///Link和Info记录不同类型区的相关信息 UINT32 shAddrAlign; /* Section alignment *///表示区的对齐单位 UINT32 shEntSize; /* Entry size if section holds table *///表示区中每个元素的大小(如果该区为一个数组的话,否则该值为0) } LDElf32Shdr; 通过readelf -S命令看看示例程序中 section header table的内容,如下图所示。示例程序共生成31个区.其实在头文件中也已经告诉我们了 Number of section headers: 31 //区数量 root@5e3abe332c5a:/home/docker/test4harmony# readelf -S app There are 31 section headers, starting at offset 0x39c0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000000318 00000318 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.propert NOTE 0000000000000338 00000338 0000000000000020 0000000000000000 A 0 0 8 [ 3] .note.gnu.build-i NOTE 0000000000000358 00000358 0000000000000024 0000000000000000 A 0 0 4 [ 4] .note.ABI-tag NOTE 000000000000037c 0000037c 0000000000000020 0000000000000000 A 0 0 4 [ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0 0000000000000024 0000000000000000 A 6 0 8 [ 6] .dynsym DYNSYM 00000000000003c8 000003c8 00000000000000a8 0000000000000018 A 7 1 8 [ 7] .dynstr STRTAB 0000000000000470 00000470 0000000000000084 0000000000000000 A 0 0 1 [ 8] .gnu.version VERSYM 00000000000004f4 000004f4 000000000000000e 0000000000000002 A 6 0 2 [ 9] .gnu.version_r VERNEED 0000000000000508 00000508 0000000000000020 0000000000000000 A 7 1 8 [10] .rela.dyn RELA 0000000000000528 00000528 00000000000000d8 0000000000000018 A 6 0 8 [11] .rela.plt RELA 0000000000000600 00000600 0000000000000018 0000000000000018 AI 6 24 8 [12] .init PROGBITS 0000000000001000 00001000 000000000000001b 0000000000000000 AX 0 0 4 [13] .plt PROGBITS 0000000000001020 00001020 0000000000000020 0000000000000010 AX 0 0 16 [14] .plt.got PROGBITS 0000000000001040 00001040 0000000000000010 0000000000000010 AX 0 0 16 [15] .plt.sec PROGBITS 0000000000001050 00001050 0000000000000010 0000000000000010 AX 0 0 16 [16] .text PROGBITS 0000000000001060 00001060 00000000000001b5 0000000000000000 AX 0 0 16 [17] .fini PROGBITS 0000000000001218 00001218 000000000000000d 0000000000000000 AX 0 0 4 [18] .rodata PROGBITS 0000000000002000 00002000 000000000000001b 0000000000000000 A 0 0 4 [19] .eh_frame_hdr PROGBITS 000000000000201c 0000201c 000000000000004c 0000000000000000 A 0 0 4 [20] .eh_frame PROGBITS 0000000000002068 00002068 0000000000000128 0000000000000000 A 0 0 8 [21] .init_array INIT_ARRAY 0000000000003db8 00002db8 0000000000000008 0000000000000008 WA 0 0 8 [22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0 0000000000000008 0000000000000008 WA 0 0 8 [23] .dynamic DYNAMIC 0000000000003dc8 00002dc8 00000000000001f0 0000000000000010 WA 7 0 8 [24] .got PROGBITS 0000000000003fb8 00002fb8 0000000000000048 0000000000000008 WA 0 0 8 [25] .data PROGBITS 0000000000004000 00003000 0000000000000018 0000000000000000 WA 0 0 8 [26] .bss NOBITS 0000000000004018 00003018 0000000000000008 0000000000000000 WA 0 0 1 [27] .comment PROGBITS 0000000000000000 00003018 000000000000002a 0000000000000001 MS 0 0 1 [28] .symtab SYMTAB 0000000000000000 00003048 0000000000000648 0000000000000018 29 46 8 [29] .strtab STRTAB 0000000000000000 00003690 0000000000000216 0000000000000000 0 0 1 [30] .shstrtab STRTAB 0000000000000000 000038a6 000000000000011a 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) String Table 从上述Section Header Table示例中,我们看到有一种类型为STRTAB的区(在Section Header Table中的下标为7,29,30)。此类区叫做String Table,其作用是集中记录字符串信息,其它区在需要使用字符串的时候,只需要记录字符串起始地址在该String Table表中的偏移即可,而无需包含整个字符串内容。 我们使用readelf -x读出下标7区的详细内容观察: root@5e3abe332c5a:/home/docker/test4harmony# readelf -x 30 app Hex dump of section '.shstrtab': 0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab 0x00000010 002e7368 73747274 6162002e 696e7465 ..shstrtab..inte 0x00000020 7270002e 6e6f7465 2e676e75 2e70726f rp..note.gnu.pro 0x00000030 70657274 79002e6e 6f74652e 676e752e perty..note.gnu. 0x00000040 6275696c 642d6964 002e6e6f 74652e41 build-id..note.A 0x00000050 42492d74 6167002e 676e752e 68617368 BI-tag..gnu.hash 0x00000060 002e6479 6e73796d 002e6479 6e737472 ..dynsym..dynstr 0x00000070 002e676e 752e7665 7273696f 6e002e67 ..gnu.version..g 0x00000080 6e752e76 65727369 6f6e5f72 002e7265 nu.version_r..re 0x00000090 6c612e64 796e002e 72656c61 2e706c74 la.dyn..rela.plt 0x000000a0 002e696e 6974002e 706c742e 676f7400 ..init..plt.got. 0x000000b0 2e706c74 2e736563 002e7465 7874002e .plt.sec..text.. 0x000000c0 66696e69 002e726f 64617461 002e6568 fini..rodata..eh 0x000000d0 5f667261 6d655f68 6472002e 65685f66 _frame_hdr..eh_f 0x000000e0 72616d65 002e696e 69745f61 72726179 rame..init_array 0x000000f0 002e6669 6e695f61 72726179 002e6479 ..fini_array..dy 0x00000100 6e616d69 63002e64 61746100 2e627373 namic..data..bss 0x00000110 002e636f 6d6d656e 7400 ..comment. 可以发现,这里其实是一堆字符串,这些字符串对应的就是各个区的名字。因此section header table中每个元素的Name字段其实是这个string table的索引。再回头看看ELF header中的e_shstrndx, Section header string table index: 30 //字符串数组索引,该区记录所有区名称 它的值正好就是30,指向了当前的string table。 再来看下29区的内容,如下所示 root@5e3abe332c5a:/home/docker/test4harmony# readelf -x 29 app Hex dump of section '.strtab': 0x00000000 00637274 73747566 662e6300 64657265 .crtstuff.c.dere 0x00000010 67697374 65725f74 6d5f636c 6f6e6573 gister_tm_clones 0x00000020 005f5f64 6f5f676c 6f62616c 5f64746f .__do_global_dto 0x00000030 72735f61 75780063 6f6d706c 65746564 rs_aux.completed 0x00000040 2e383036 30005f5f 646f5f67 6c6f6261 .8060.__do_globa 0x00000050 6c5f6474 6f72735f 6175785f 66696e69 l_dtors_aux_fini 0x00000060 5f617272 61795f65 6e747279 00667261 _array_entry.fra 0x00000070 6d655f64 756d6d79 005f5f66 72616d65 me_dummy.__frame 0x00000080 5f64756d 6d795f69 6e69745f 61727261 _dummy_init_arra 0x00000090 795f656e 74727900 6d61696e 2e63005f y_entry.main.c._ 0x000000a0 5f465241 4d455f45 4e445f5f 005f5f69 _FRAME_END__.__i 0x000000b0 6e69745f 61727261 795f656e 64005f44 nit_array_end._D 0x000000c0 594e414d 4943005f 5f696e69 745f6172 YNAMIC.__init_ar 0x000000d0 7261795f 73746172 74005f5f 474e555f ray_start.__GNU_ 0x000000e0 45485f46 52414d45 5f484452 005f474c EH_FRAME_HDR._GL 0x000000f0 4f42414c 5f4f4646 5345545f 5441424c OBAL_OFFSET_TABL 0x00000100 455f005f 5f6c6962 635f6373 755f6669 E_.__libc_csu_fi 0x00000110 6e69006d 795f6e61 6d65005f 49544d5f ni.my_name._ITM_ 0x00000120 64657265 67697374 6572544d 436c6f6e deregisterTMClon 0x00000130 65546162 6c65005f 65646174 61007072 eTable._edata.pr 0x00000140 696e7466 4040474c 4942435f 322e322e intf@@GLIBC_2.2. 0x00000150 35005f5f 6c696263 5f737461 72745f6d 5.__libc_start_m 0x00000160 61696e40 40474c49 42435f32 2e322e35 ain@@GLIBC_2.2.5 0x00000170 005f5f64 6174615f 73746172 74005f5f .__data_start.__ 0x00000180 676d6f6e 5f737461 72745f5f 005f5f64 gmon_start__.__d 0x00000190 736f5f68 616e646c 65005f49 4f5f7374 so_handle._IO_st 0x000001a0 64696e5f 75736564 005f5f6c 6962635f din_used.__libc_ 0x000001b0 6373755f 696e6974 005f5f62 73735f73 csu_init.__bss_s 0x000001c0 74617274 006d6169 6e007361 795f6865 tart.main.say_he 0x000001d0 6c6c6f00 5f5f544d 435f454e 445f5f00 llo.__TMC_END__. 0x000001e0 5f49544d 5f726567 69737465 72544d43 _ITM_registerTMC 0x000001f0 6c6f6e65 5461626c 65005f5f 6378615f loneTable.__cxa_ 0x00000200 66696e61 6c697a65 4040474c 4942435f finalize@@GLIBC_ 0x00000210 322e322e 3500 2.2.5. 这里我们看到了main、say_hello字符串,这些是我们在示例中源码中定义的符号,由此29区是应用自身的String Table,记录了应用使用的字符串。这个表就是符号表的一部分,通过readelf -s命令能拿到全部符号信息.如下 符号表 Symbol Table Section Header Table中,还有一类SYMTAB(DYNSYM)区,该区叫符号表。符号表中的每个元素对应一个符号,记录了每个符号对应的实际数值信息,通常用在重定位过程中或问题定位过程中,进程执行阶段并不加载符号表。符号表中每个元素定义看注解。 符号表对应鸿蒙源码结构体为 LDElf32Sym, //kernel\extended\dynload\include\los_ld_elf_pri.h /* Symbol table */ typedef struct { UINT32 stName; /* Symbol table name (string tbl index) *///表示符号对应的源码字符串,为对应String Table中的索引 UINT32 stValue; /* Symbol table value *///表示符号对应的数值 UINT32 stSize; /* Symbol table size *///表示符号对应数值的空间占用大小 UINT8 stInfo; /* Symbol table type and binding *///表示符号的相关信息 如符号类型(变量符号、函数符号) UINT8 stOther; /* Symbol table visibility */ UINT16 stShndx; /* Section table index *///表示与该符号相关的区的索引,例如函数符号与对应的代码区相关 } LDElf32Sym; 我们用readelf -s读出示例程序中的符号表,如下所示 root@5e3abe332c5a:/home/docker/test4harmony# readelf -s app Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) Symbol table '.symtab' contains 67 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000318 0 SECTION LOCAL DEFAULT 1 2: 0000000000000338 0 SECTION LOCAL DEFAULT 2 3: 0000000000000358 0 SECTION LOCAL DEFAULT 3 4: 000000000000037c 0 SECTION LOCAL DEFAULT 4 5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5 6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6 7: 0000000000000470 0 SECTION LOCAL DEFAULT 7 8: 00000000000004f4 0 SECTION LOCAL DEFAULT 8 9: 0000000000000508 0 SECTION LOCAL DEFAULT 9 10: 0000000000000528 0 SECTION LOCAL DEFAULT 10 11: 0000000000000600 0 SECTION LOCAL DEFAULT 11 12: 0000000000001000 0 SECTION LOCAL DEFAULT 12 13: 0000000000001020 0 SECTION LOCAL DEFAULT 13 14: 0000000000001040 0 SECTION LOCAL DEFAULT 14 15: 0000000000001050 0 SECTION LOCAL DEFAULT 15 16: 0000000000001060 0 SECTION LOCAL DEFAULT 16 17: 0000000000001218 0 SECTION LOCAL DEFAULT 17 18: 0000000000002000 0 SECTION LOCAL DEFAULT 18 19: 000000000000201c 0 SECTION LOCAL DEFAULT 19 20: 0000000000002068 0 SECTION LOCAL DEFAULT 20 21: 0000000000003db8 0 SECTION LOCAL DEFAULT 21 22: 0000000000003dc0 0 SECTION LOCAL DEFAULT 22 23: 0000000000003dc8 0 SECTION LOCAL DEFAULT 23 24: 0000000000003fb8 0 SECTION LOCAL DEFAULT 24 25: 0000000000004000 0 SECTION LOCAL DEFAULT 25 26: 0000000000004018 0 SECTION LOCAL DEFAULT 26 27: 0000000000000000 0 SECTION LOCAL DEFAULT 27 28: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 29: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones 30: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones 31: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux 32: 0000000000004018 1 OBJECT LOCAL DEFAULT 26 completed.8060 33: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fin 34: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy 35: 0000000000003db8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_ 36: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c 37: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 38: 000000000000218c 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__ 39: 0000000000000000 0 FILE LOCAL DEFAULT ABS 40: 0000000000003dc0 0 NOTYPE LOCAL DEFAULT 21 __init_array_end 41: 0000000000003dc8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC 42: 0000000000003db8 0 NOTYPE LOCAL DEFAULT 21 __init_array_start 43: 000000000000201c 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR 44: 0000000000003fb8 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_ 45: 0000000000001000 0 FUNC LOCAL DEFAULT 12 _init 46: 0000000000001210 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini 47: 0000000000004010 8 OBJECT GLOBAL DEFAULT 25 my_name 48: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 49: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start 50: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 25 _edata 51: 0000000000001218 0 FUNC GLOBAL HIDDEN 17 _fini 52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5 53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 54: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start 55: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 56: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle 57: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used 58: 00000000000011a0 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init 59: 0000000000004020 0 NOTYPE GLOBAL DEFAULT 26 _end 60: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start 61: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 __bss_start 62: 0000000000001174 30 FUNC GLOBAL DEFAULT 16 main 63: 0000000000001149 43 FUNC GLOBAL DEFAULT 16 say_hello 64: 0000000000004018 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__ 65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 66: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2 在最后位置找到了亲切的 main和say_hello 62: 0000000000001174 30 FUNC GLOBAL DEFAULT 16 main 63: 0000000000001149 43 FUNC GLOBAL DEFAULT 16 say_hello main函数符号对应的数值为0x1174,其类型为FUNC,大小为30字节,对应的代码区索引为16; say_hello函数符号对应数值为0x1149,其类型为FUNC,大小为43字节,对应的代码区索引同为16。 Section Header Table: [16] .text PROGBITS 0000000000001060 00001060 00000000000001b5 0000000000000000 AX 0 0 16 反汇编代码区 在理解了String Table和Symbol Table的作用后,我们通过objdump反汇编来理解一下.text代码区: root@5e3abe332c5a:/home/docker/test4harmony# objdump -j .text -l -C -S app 0000000000001149 <say_hello>: say_hello(): 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 83 ec 10 sub $0x10,%rsp 1155: 48 89 7d f8 mov %rdi,-0x8(%rbp) 1159: 48 8b 45 f8 mov -0x8(%rbp),%rax 115d: 48 89 c6 mov %rax,%rsi 1160: 48 8d 3d 9d 0e 00 00 lea 0xe9d(%rip),%rdi # 2004 <_IO_stdin_used+0x4> 1167: b8 00 00 00 00 mov $0x0,%eax 116c: e8 df fe ff ff callq 1050 <printf@plt> 1171: 90 nop 1172: c9 leaveq 1173: c3 retq 0000000000001174 <main>: main(): 1174: f3 0f 1e fa endbr64 1178: 55 push %rbp 1179: 48 89 e5 mov %rsp,%rbp 117c: 48 8b 05 8d 2e 00 00 mov 0x2e8d(%rip),%rax # 4010 <my_name> 1183: 48 89 c7 mov %rax,%rdi 1186: e8 be ff ff ff callq 1149 <say_hello> 118b: b8 00 00 00 00 mov $0x0,%eax 1190: 5d pop %rbp 1191: c3 retq 1192: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 1199: 00 00 00 119c: 0f 1f 40 00 nopl 0x0(%rax) 0x1149 0x1174正是say_hello,main函数的入口地址.我们看到了激动人心的调用1149位置的say_hello指令 1186: e8 be ff ff ff callq 1149 <say_hello> 看了这么久还记得开头的C代码的样子吗? 再看一遍 : ) #include <stdio.h> void say_hello(char *who) { printf("hello, %s!\n", who); } char *my_name = "harmony os"; int main() { say_hello(my_name); return 0; } root@5e3abe332c5a:/home/docker/test4harmony# ./app hello, harmony os! 但是!!! 晕,怎么还有but,西卡西...还有完没完.上面让大家记住的还有一个地方没说到 Entry point address: 0x1060 //代码区 .text 起始位置,即程序运行开始位置 它的地址并不是main函数位置0x1174,是0x1060!而且代码区的开始位置是0x1060没错. [16] .text PROGBITS 0000000000001060 00001060 00000000000001b5 0000000000000000 AX 0 0 16 难度main不是入口地址? 那0x1060上放的是何方神圣,再查符号表发现是 60: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start 从反汇编堆中找到 _start 0000000000001060 <_start>: _start(): 1060: f3 0f 1e fa endbr64 1064: 31 ed xor %ebp,%ebp 1066: 49 89 d1 mov %rdx,%r9 1069: 5e pop %rsi 106a: 48 89 e2 mov %rsp,%rdx 106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 1071: 50 push %rax 1072: 54 push %rsp 1073: 4c 8d 05 96 01 00 00 lea 0x196(%rip),%r8 # 1210 <__libc_csu_fini> 107a: 48 8d 0d 1f 01 00 00 lea 0x11f(%rip),%rcx # 11a0 <__libc_csu_init> 1081: 48 8d 3d ec 00 00 00 lea 0xec(%rip),%rdi # 1174 <main> 1088: ff 15 52 2f 00 00 callq *0x2f52(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5> 108e: f4 hlt 108f: 90 nop 这才看到了亲切的0x1174的main函数.所以真正的说法是: 从内核动态加载的视角看,程序运行首个函数并不是main,而是_start. 但从应用程序开发者视角看,main就是启动函数. 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v51.xx (ELF反汇编篇) | 程序的入口函数并不是main < csdn | 51cto | osc > v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | osc > v49.xx (信号消费篇) | 两次切换用户栈和内核栈 < csdn | 51cto | osc > v48.xx (信号生产篇) | 如何安装和发送异步信号 < csdn | 51cto | osc > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | osc > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | osc > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 < csdn | 51cto | osc > v44.xx (中断管理篇) | 硬中断的实现像观察者模式 < csdn | 51cto | osc > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | osc > v42.xx (中断切换篇) | 中断切换又在切换什么 < csdn | 51cto | osc > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | osc > v40.xx (汇编汇总篇) | 所有的汇编代码都在呢 < csdn | 51cto | osc > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | osc > v38.xx (寄存器篇) | 看完再也不对寄存器怕怕 < csdn | 51cto | osc > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | osc > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆 < csdn | 51cto | osc > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | osc > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航 < csdn | 51cto | osc > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 < csdn | 51cto | osc > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | osc > v31.xx (定时器篇) | 内核最高优先级任务是谁 < csdn | 51cto | osc > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | osc > v29.xx (信号量篇) | 信号量解决任务的什么问题 < csdn | 51cto | osc > v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | osc > v27.xx (互斥锁篇) | 比自旋锁丰满的互斥锁 < csdn | 51cto | osc > v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊 < csdn | 51cto | osc > v25.xx (并发并行篇) | 两个字永远记住二者区别 < csdn | 51cto | osc > v24.xx (进程概念篇) | 进程在管理哪些资源 < csdn | 51cto | osc > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数 < csdn | 51cto | osc > v22.xx (汇编基础篇) | cpu在哪里打卡上班 < csdn | 51cto | osc > v21.xx (线程概念篇) | 是谁在不断的折腾cpu < csdn | 51cto | osc > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | osc > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个 < csdn | 51cto | osc > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | osc > v17.xx (物理内存篇) | 怎么管理物理内存 < csdn | 51cto | osc > v16.xx (内存规则篇) | 内存管理到底在管什么 < csdn | 51cto | osc > v15.xx (内存映射篇) | 虚拟内存虚在哪里 < csdn | 51cto | osc > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础 < csdn | 51cto | osc > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | osc > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的 < csdn | 51cto | osc > v11.xx (内存分配篇) | 内存有哪些分配方式 < csdn | 51cto | osc > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处 < csdn | 51cto | osc > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | osc > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | osc > v07.xx (调度机制篇) | 任务是如何被调度执行的 < csdn | 51cto | osc > v06.xx (调度队列篇) | 内核有多少个调度队列 < csdn | 51cto | osc > v05.xx (任务管理篇) | 任务池是如何管理的 < csdn | 51cto | osc > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | osc > v03.xx (时钟任务篇) | 谁是触发调度的最大动力 < csdn | 51cto | osc > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | osc > v01.xx (双向链表篇) | 谁是内核最重要结构体 < csdn | 51cto | osc > 进入 >> osc | csdn | 51cto | 掘金 | 公众号 | 头条号 | gitee | github 鸿蒙内核源码分析系列篇 . 原创不易,欢迎转载,但请注明出处. 热爱是所有的理由和答案 - turing 不经过深度思考的人生不值得一过 - turing

优秀的个人博客,低调大师

鸿蒙内核源码分析(ELF反汇编篇) | 程序的入口函数并不是main | 百篇博客分析HarmonyOS源码 | v51.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< osc | 51cto | csdn | harmony > 阅读之前的说明 先说明,本篇很长,也很枯燥,若不是绝对的技术偏执狂是看不下去的.将通过一段简单代码去跟踪编译成ELF格式后的内容.看看ELF究竟长了怎样的一副花花肠子并用了 readelf命令去窥视ELF的全貌,最后用objdump命令反汇编ELF.找到了大家熟悉画面. 示例代码 在windows环境 E:\harmony\docker\test4harmony下创建 main.c文件,内容如下: #include <stdio.h> void say_hello(char *who) { printf("hello, %s!\n", who); } char *my_name = "harmony os"; int main() { say_hello(my_name); return 0; } 因在 v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | osc > 中做好了映射,所以会同时出现在docker中. 编译生成ELF运行,并用readelf -h查看app头部信息. root@5e3abe332c5a:/home/docker/test4harmony# ls main.c root@5e3abe332c5a:/home/docker/test4harmony# gcc -o app main.c root@5e3abe332c5a:/home/docker/test4harmony# ls app main.c root@5e3abe332c5a:/home/docker/test4harmony# ./app hello, harmony os! root@5e3abe332c5a:/home/docker/test4harmony# readelf -h app ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x1060 Start of program headers: 64 (bytes into file) Start of section headers: 14784 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 13 Size of section headers: 64 (bytes) Number of section headers: 31 Section header string table index: 30 这里留意这几个内容,下面会说明,先记住. Entry point address: 0x1060 //代码段 .text 起始位置,即程序运行开始位置 Number of program headers: 13 //段数量 Number of section headers: 31 //区数量 Section header string table index: 30 //字符串数组索引,该区记录所有区名称 ELF历史 ELF的英文全称是The Executable and Linking Format,最初是由UNIX系统实验室开发、发布的ABI(Application Binary Interface)接口的一部分,也是Linux的主要可执行文件格式。 ELF整体布局 ELF格式可以表达四种类型的二进制对象文件(object files): 可重定位文件(relocatable file,就是大家平常见到的.o文件)包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 可执行文件(executable file, 例上述示例代码生成的app文件)包含代码和数据,是可以直接运行的程序。其代码和数据都有固定的地址 (或相对于基地址的偏移 ),系统可根据这些地址信息把程序加载到内存执行。 共享库文件(shared object file,就是.so文件,用来做动态链接)也称动态库文件,包含了代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.l、libc.so.l、ld-linux.so.l)使用的。 核心转储文件(core dump file,就是core dump文件) 可重定位文件用在编译和链接阶段;可执行文件用在程序运行阶段;共享库则同时用在编译链接和运行阶段。在不同阶段,我们可以用不同视角来理解ELF文件,整体布局如下图所示: 从上图可见,ELF格式文件整体可分为四大部分: ELF Header, ELF头部,定义全局性信息,即readelf -h app看到的内容 Program Header Table, 描述段(Segment)信息的数组,每个元素对应一个段;通常包含在可执行文件中,可重定文件中可选(通常不包含) 即readelf -l app看到的前半部分内容 Segment and Section,段(Segment)由若干区(Section)组成;段在运行时被加载到进程地址空间中,包含在可执行文件中;区是段的组成单元,包含在可执行文件和可重定位文件中 即readelf -l app后半部分内容,并readelf -x ** app查看区具体内容. Section Header Table,描述区(Section)信息的数组,每个元素对应一个区;通常包含在可重定位文件中,可执行文件中为可选(通常包含) 即readelf -S app看到的内容 ELF头信息 ELF头部信息对应鸿蒙源码结构体为 LDElf32Ehdr, 各字段含义已一一注解,很容易理解. //kernel\extended\dynload\include\los_ld_elf_pri.h /* Elf header */ #define LD_EI_NIDENT 16 typedef struct { UINT8 elfIdent[LD_EI_NIDENT]; /* Magic number and other info *///含前16个字节,又可细分成class、data、version等字段,具体含义不用太关心,只需知道前4个字节点包含”ELF”关键字,这样可以判断当前文件是否是ELF格式; UINT16 elfType; /* Object file type *///表示具体ELF类型,可重定位文件/可执行文件/共享库文件,显然这里是一个可执行文件;e_machine表示执行的机器平台,这里是x86_64; UINT16 elfMachine; /* Architecture *///表示cpu架构 UINT32 elfVersion; /* Object file version *///表示文件版本号,这里的1表示初始版本号; UINT32 elfEntry; /* Entry point virtual address *///对应”Entry point address”,程序入口函数地址,通过进程虚拟地址空间地址(0x400440)表达; UINT32 elfPhoff; /* Program header table file offset *///对应“Start of program headers”,表示program header table在文件内的偏移位置,这里是从第64号字节(假设初始为0号字节)开始; UINT32 elfShoff; /* Section header table file offset *///对应”Start of section headers”,表示section header table在文件内的偏移位置,这里是从第4472号字节开始,靠近文件尾部; UINT32 elfFlags; /* Processor-specific flags *///表示与CPU处理器架构相关的信息,这里为零; UINT16 elfHeadSize; /* ELF header size in bytes *///对应”Size of this header”,表示本ELF header自身的长度,这里为64个字节,回看前面的e_phoff为64,说明ELF header后紧跟着program header table; UINT16 elfPhEntSize; /* Program header table entry size *///对应“Size of program headers”,表示program header table中每个元素的大小,这里为56个字节; UINT16 elfPhNum; /* Program header table entry count *///对应”Number of program headers”,表示program header table中元素个数,这里为9个; UINT16 elfShEntSize; /* Section header table entry size *///e_shentsize 对应”Size of section headers”,表示section header table中每个元素的大小,这里为64个字节; UINT16 elfShNum; /* Section header table entry count *///e_shnum 对应”Number of section headers”,表示section header table中元素的个数,这里为30个; UINT16 elfShStrIndex; /* Section header string table index *///e_shstrndx 对应”Section header string table index”,表示描述各section字符名称的string table在section header table中的下标,详见后文对string table的介绍。 } LDElf32Ehdr; 结构体对应readelf -h app命令内容来理解,就不再贴出来了. 段(Segment)头信息 段(Segment)信息对应鸿蒙源码结构体为 LDElf32Phdr, //kernel\extended\dynload\include\los_ld_elf_pri.h /* Program Header */ typedef struct { UINT32 type; /* Segment type */ UINT32 offset; /* Segment file offset */ UINT32 vAddr; /* Segment virtual address */ UINT32 phyAddr; /* Segment physical address */ UINT32 fileSize; /* Segment size in file */ UINT32 memSize; /* Segment size in memory */ UINT32 flags; /* Segment flags */ UINT32 align; /* Segment alignment */ } LDElf32Phdr; 用readelf -l查看app段头部表 root@5e3abe332c5a:/home/docker/test4harmony# readelf -l app Elf file type is DYN (Shared object file) Entry point 0x1060 There are 13 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8 ......... 解读 先看命令返回的前半部分: Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8 INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000618 0x0000000000000618 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000225 0x0000000000000225 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000190 0x0000000000000190 R 0x1000 LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000260 0x0000000000000268 RW 0x1000 DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8 0x00000000000001f0 0x00000000000001f0 RW 0x8 NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358 0x0000000000000044 0x0000000000000044 R 0x4 GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338 0x0000000000000020 0x0000000000000020 R 0x8 GNU_EH_FRAME 0x000000000000201c 0x000000000000201c 0x000000000000201c 0x000000000000004c 0x000000000000004c R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8 0x0000000000000248 0x0000000000000248 R 0x1 数一下一共13个段,其实在ELF头信息也告诉了我们共13个段 Number of program headers: 13 //段数量 PHDR,此类型header元素描述了program header table自身的信息。从这里的内容看出,示例程序的program header table在文件中的偏移(Offset)为0x40,即64号字节处;该段映射到进程空间的虚拟地址(VirtAddr)为0x40;PhysAddr暂时不用,其保持和VirtAddr一致;该段占用的文件大小FileSiz为0x2d8;运行时占用进程空间内存大小MemSiz也为0x2d8;Flags标记表示该段的读写权限,这里”R E”表示可读可执行,说明本段属于代码段;Align对齐为8,表明本段按8字节对齐。 INTERP,此类型header元素描述了一个特殊内存段,该段内存记录了动态加载解析器的访问路径字符串。示例程序中,该段内存位于文件偏移0x318处,即紧跟program header table;映射的进程虚拟地址空间地址为0x318;文件长度和内存映射长度均为0x1c,即28个字符,具体内容为”/lib64/ld-linux-x86-64.so.2”;段属性为只读,并按字节对齐; LOAD,此类型header元素描述了可加载到进程空间的代码段或数据段: 第四段为代码段,文件内偏移为0x1000,映射到进程地址0x001000处,代码段长度为0x618个字节,属性为只读可执行(RE),段地址按0x1000(4K)边界对齐; 第六段为数据段,文件内偏移为0x1000,映射到进程地址为0x1000处(按2M对齐移动),文件大小为0x230,内存大小为0x238(因为其内部包含了8字节的bss段,即未初始化数据段,该段内容不占文件空间,但在运行时需要为其分配空间并清零),属性为读写(RW),段地址也按0x1000(4K)边界对齐。 DYNAMIC,此类型header元素描述了动态加载段,其内部通常包含了一个名为”.dynamic”的动态加载区;这也是一个数组,每个元素描述了与动态加载相关的各方面信息,将在系列篇(动态加载篇)中介绍。该段是从文件偏移0x2dc8处开始,长度为0x1f0,并映射到进程的0x3dc8;可见该段和上一个数据段是有重叠的。 再看命令返回内容的后半部分-段区映射关系 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.got .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .dynamic .got 段和区是一对多的关系,一个段可包含多个区 INTERP段只包含了.interp区 代码段包含.interp、.plt、.text等区 数据段包含.dynamic、.data、.bss等区 DYNAMIC`段包含”.dynamic”区。从这里可以看出,有些区被包含在多个段中。 区表 段(Segment)信息对应鸿蒙源码结构体为 LDElf32Shdr, //kernel\extended\dynload\include\los_ld_elf_pri.h /* Section header */ typedef struct { UINT32 shName; /* Section name (string tbl index) *///表示每个区的名字 UINT32 shType; /* Section type *///表示每个区的功能 UINT32 shFlags; /* Section flags *///表示每个区的属性 UINT32 shAddr; /* Section virtual addr at execution *///表示每个区的进程映射地址 UINT32 shOffset; /* Section file offset *///表示文件内偏移 UINT32 shSize; /* Section size in bytes *///表示区的大小 UINT32 shLink; /* Link to another section *///Link和Info记录不同类型区的相关信息 UINT32 shInfo; /* Additional section information *///Link和Info记录不同类型区的相关信息 UINT32 shAddrAlign; /* Section alignment *///表示区的对齐单位 UINT32 shEntSize; /* Entry size if section holds table *///表示区中每个元素的大小(如果该区为一个数组的话,否则该值为0) } LDElf32Shdr; 通过readelf -S命令看看示例程序中section header table的内容,如下图所示。示例程序共生成31个区.其实在头文件中也已经告诉我们了 Number of section headers: 31 //区数量 root@5e3abe332c5a:/home/docker/test4harmony# readelf -S app There are 31 section headers, starting at offset 0x39c0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000000318 00000318 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.gnu.propert NOTE 0000000000000338 00000338 0000000000000020 0000000000000000 A 0 0 8 [ 3] .note.gnu.build-i NOTE 0000000000000358 00000358 0000000000000024 0000000000000000 A 0 0 4 [ 4] .note.ABI-tag NOTE 000000000000037c 0000037c 0000000000000020 0000000000000000 A 0 0 4 [ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0 0000000000000024 0000000000000000 A 6 0 8 [ 6] .dynsym DYNSYM 00000000000003c8 000003c8 00000000000000a8 0000000000000018 A 7 1 8 [ 7] .dynstr STRTAB 0000000000000470 00000470 0000000000000084 0000000000000000 A 0 0 1 [ 8] .gnu.version VERSYM 00000000000004f4 000004f4 000000000000000e 0000000000000002 A 6 0 2 [ 9] .gnu.version_r VERNEED 0000000000000508 00000508 0000000000000020 0000000000000000 A 7 1 8 [10] .rela.dyn RELA 0000000000000528 00000528 00000000000000d8 0000000000000018 A 6 0 8 [11] .rela.plt RELA 0000000000000600 00000600 0000000000000018 0000000000000018 AI 6 24 8 [12] .init PROGBITS 0000000000001000 00001000 000000000000001b 0000000000000000 AX 0 0 4 [13] .plt PROGBITS 0000000000001020 00001020 0000000000000020 0000000000000010 AX 0 0 16 [14] .plt.got PROGBITS 0000000000001040 00001040 0000000000000010 0000000000000010 AX 0 0 16 [15] .plt.sec PROGBITS 0000000000001050 00001050 0000000000000010 0000000000000010 AX 0 0 16 [16] .text PROGBITS 0000000000001060 00001060 00000000000001b5 0000000000000000 AX 0 0 16 [17] .fini PROGBITS 0000000000001218 00001218 000000000000000d 0000000000000000 AX 0 0 4 [18] .rodata PROGBITS 0000000000002000 00002000 000000000000001b 0000000000000000 A 0 0 4 [19] .eh_frame_hdr PROGBITS 000000000000201c 0000201c 000000000000004c 0000000000000000 A 0 0 4 [20] .eh_frame PROGBITS 0000000000002068 00002068 0000000000000128 0000000000000000 A 0 0 8 [21] .init_array INIT_ARRAY 0000000000003db8 00002db8 0000000000000008 0000000000000008 WA 0 0 8 [22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0 0000000000000008 0000000000000008 WA 0 0 8 [23] .dynamic DYNAMIC 0000000000003dc8 00002dc8 00000000000001f0 0000000000000010 WA 7 0 8 [24] .got PROGBITS 0000000000003fb8 00002fb8 0000000000000048 0000000000000008 WA 0 0 8 [25] .data PROGBITS 0000000000004000 00003000 0000000000000018 0000000000000000 WA 0 0 8 [26] .bss NOBITS 0000000000004018 00003018 0000000000000008 0000000000000000 WA 0 0 1 [27] .comment PROGBITS 0000000000000000 00003018 000000000000002a 0000000000000001 MS 0 0 1 [28] .symtab SYMTAB 0000000000000000 00003048 0000000000000648 0000000000000018 29 46 8 [29] .strtab STRTAB 0000000000000000 00003690 0000000000000216 0000000000000000 0 0 1 [30] .shstrtab STRTAB 0000000000000000 000038a6 000000000000011a 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) String Table 从上述Section Header Table示例中,我们看到有一种类型为STRTAB的区(在Section Header Table中的下标为7,29,30)。此类区叫做String Table,其作用是集中记录字符串信息,其它区在需要使用字符串的时候,只需要记录字符串起始地址在该String Table表中的偏移即可,而无需包含整个字符串内容。 我们使用readelf -x读出下标7区的详细内容观察: root@5e3abe332c5a:/home/docker/test4harmony# readelf -x 30 app Hex dump of section '.shstrtab': 0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab 0x00000010 002e7368 73747274 6162002e 696e7465 ..shstrtab..inte 0x00000020 7270002e 6e6f7465 2e676e75 2e70726f rp..note.gnu.pro 0x00000030 70657274 79002e6e 6f74652e 676e752e perty..note.gnu. 0x00000040 6275696c 642d6964 002e6e6f 74652e41 build-id..note.A 0x00000050 42492d74 6167002e 676e752e 68617368 BI-tag..gnu.hash 0x00000060 002e6479 6e73796d 002e6479 6e737472 ..dynsym..dynstr 0x00000070 002e676e 752e7665 7273696f 6e002e67 ..gnu.version..g 0x00000080 6e752e76 65727369 6f6e5f72 002e7265 nu.version_r..re 0x00000090 6c612e64 796e002e 72656c61 2e706c74 la.dyn..rela.plt 0x000000a0 002e696e 6974002e 706c742e 676f7400 ..init..plt.got. 0x000000b0 2e706c74 2e736563 002e7465 7874002e .plt.sec..text.. 0x000000c0 66696e69 002e726f 64617461 002e6568 fini..rodata..eh 0x000000d0 5f667261 6d655f68 6472002e 65685f66 _frame_hdr..eh_f 0x000000e0 72616d65 002e696e 69745f61 72726179 rame..init_array 0x000000f0 002e6669 6e695f61 72726179 002e6479 ..fini_array..dy 0x00000100 6e616d69 63002e64 61746100 2e627373 namic..data..bss 0x00000110 002e636f 6d6d656e 7400 ..comment. 可以发现,这里其实是一堆字符串,这些字符串对应的就是各个区的名字。因此section header table中每个元素的Name字段其实是这个string table的索引。再回头看看ELF header中的e_shstrndx, Section header string table index: 30 //字符串数组索引,该区记录所有区名称 它的值正好就是30,指向了当前的string table。 再来看下29区的内容,如下所示 root@5e3abe332c5a:/home/docker/test4harmony# readelf -x 29 app Hex dump of section '.strtab': 0x00000000 00637274 73747566 662e6300 64657265 .crtstuff.c.dere 0x00000010 67697374 65725f74 6d5f636c 6f6e6573 gister_tm_clones 0x00000020 005f5f64 6f5f676c 6f62616c 5f64746f .__do_global_dto 0x00000030 72735f61 75780063 6f6d706c 65746564 rs_aux.completed 0x00000040 2e383036 30005f5f 646f5f67 6c6f6261 .8060.__do_globa 0x00000050 6c5f6474 6f72735f 6175785f 66696e69 l_dtors_aux_fini 0x00000060 5f617272 61795f65 6e747279 00667261 _array_entry.fra 0x00000070 6d655f64 756d6d79 005f5f66 72616d65 me_dummy.__frame 0x00000080 5f64756d 6d795f69 6e69745f 61727261 _dummy_init_arra 0x00000090 795f656e 74727900 6d61696e 2e63005f y_entry.main.c._ 0x000000a0 5f465241 4d455f45 4e445f5f 005f5f69 _FRAME_END__.__i 0x000000b0 6e69745f 61727261 795f656e 64005f44 nit_array_end._D 0x000000c0 594e414d 4943005f 5f696e69 745f6172 YNAMIC.__init_ar 0x000000d0 7261795f 73746172 74005f5f 474e555f ray_start.__GNU_ 0x000000e0 45485f46 52414d45 5f484452 005f474c EH_FRAME_HDR._GL 0x000000f0 4f42414c 5f4f4646 5345545f 5441424c OBAL_OFFSET_TABL 0x00000100 455f005f 5f6c6962 635f6373 755f6669 E_.__libc_csu_fi 0x00000110 6e69006d 795f6e61 6d65005f 49544d5f ni.my_name._ITM_ 0x00000120 64657265 67697374 6572544d 436c6f6e deregisterTMClon 0x00000130 65546162 6c65005f 65646174 61007072 eTable._edata.pr 0x00000140 696e7466 4040474c 4942435f 322e322e intf@@GLIBC_2.2. 0x00000150 35005f5f 6c696263 5f737461 72745f6d 5.__libc_start_m 0x00000160 61696e40 40474c49 42435f32 2e322e35 ain@@GLIBC_2.2.5 0x00000170 005f5f64 6174615f 73746172 74005f5f .__data_start.__ 0x00000180 676d6f6e 5f737461 72745f5f 005f5f64 gmon_start__.__d 0x00000190 736f5f68 616e646c 65005f49 4f5f7374 so_handle._IO_st 0x000001a0 64696e5f 75736564 005f5f6c 6962635f din_used.__libc_ 0x000001b0 6373755f 696e6974 005f5f62 73735f73 csu_init.__bss_s 0x000001c0 74617274 006d6169 6e007361 795f6865 tart.main.say_he 0x000001d0 6c6c6f00 5f5f544d 435f454e 445f5f00 llo.__TMC_END__. 0x000001e0 5f49544d 5f726567 69737465 72544d43 _ITM_registerTMC 0x000001f0 6c6f6e65 5461626c 65005f5f 6378615f loneTable.__cxa_ 0x00000200 66696e61 6c697a65 4040474c 4942435f finalize@@GLIBC_ 0x00000210 322e322e 3500 2.2.5. 这里我们看到了main、say_hello字符串,这些是我们在示例中源码中定义的符号,由此29区是应用自身的String Table,记录了应用使用的字符串。这个表就是符号表的一部分,通过readelf -s命令能拿到全部符号信息.如下 符号表 Symbol Table Section Header Table中,还有一类SYMTAB(DYNSYM)区,该区叫符号表。符号表中的每个元素对应一个符号,记录了每个符号对应的实际数值信息,通常用在重定位过程中或问题定位过程中,进程执行阶段并不加载符号表。符号表中每个元素定义看注解。 符号表对应鸿蒙源码结构体为 LDElf32Sym, //kernel\extended\dynload\include\los_ld_elf_pri.h /* Symbol table */ typedef struct { UINT32 stName; /* Symbol table name (string tbl index) *///表示符号对应的源码字符串,为对应String Table中的索引 UINT32 stValue; /* Symbol table value *///表示符号对应的数值 UINT32 stSize; /* Symbol table size *///表示符号对应数值的空间占用大小 UINT8 stInfo; /* Symbol table type and binding *///表示符号的相关信息 如符号类型(变量符号、函数符号) UINT8 stOther; /* Symbol table visibility */ UINT16 stShndx; /* Section table index *///表示与该符号相关的区的索引,例如函数符号与对应的代码区相关 } LDElf32Sym; 我们用readelf -s读出示例程序中的符号表,如下所示 root@5e3abe332c5a:/home/docker/test4harmony# readelf -s app Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) Symbol table '.symtab' contains 67 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000318 0 SECTION LOCAL DEFAULT 1 2: 0000000000000338 0 SECTION LOCAL DEFAULT 2 3: 0000000000000358 0 SECTION LOCAL DEFAULT 3 4: 000000000000037c 0 SECTION LOCAL DEFAULT 4 5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5 6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6 7: 0000000000000470 0 SECTION LOCAL DEFAULT 7 8: 00000000000004f4 0 SECTION LOCAL DEFAULT 8 9: 0000000000000508 0 SECTION LOCAL DEFAULT 9 10: 0000000000000528 0 SECTION LOCAL DEFAULT 10 11: 0000000000000600 0 SECTION LOCAL DEFAULT 11 12: 0000000000001000 0 SECTION LOCAL DEFAULT 12 13: 0000000000001020 0 SECTION LOCAL DEFAULT 13 14: 0000000000001040 0 SECTION LOCAL DEFAULT 14 15: 0000000000001050 0 SECTION LOCAL DEFAULT 15 16: 0000000000001060 0 SECTION LOCAL DEFAULT 16 17: 0000000000001218 0 SECTION LOCAL DEFAULT 17 18: 0000000000002000 0 SECTION LOCAL DEFAULT 18 19: 000000000000201c 0 SECTION LOCAL DEFAULT 19 20: 0000000000002068 0 SECTION LOCAL DEFAULT 20 21: 0000000000003db8 0 SECTION LOCAL DEFAULT 21 22: 0000000000003dc0 0 SECTION LOCAL DEFAULT 22 23: 0000000000003dc8 0 SECTION LOCAL DEFAULT 23 24: 0000000000003fb8 0 SECTION LOCAL DEFAULT 24 25: 0000000000004000 0 SECTION LOCAL DEFAULT 25 26: 0000000000004018 0 SECTION LOCAL DEFAULT 26 27: 0000000000000000 0 SECTION LOCAL DEFAULT 27 28: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 29: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones 30: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones 31: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux 32: 0000000000004018 1 OBJECT LOCAL DEFAULT 26 completed.8060 33: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fin 34: 0000000000001140 0 FUNC LOCAL DEFAULT 16 frame_dummy 35: 0000000000003db8 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_ 36: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c 37: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 38: 000000000000218c 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__ 39: 0000000000000000 0 FILE LOCAL DEFAULT ABS 40: 0000000000003dc0 0 NOTYPE LOCAL DEFAULT 21 __init_array_end 41: 0000000000003dc8 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC 42: 0000000000003db8 0 NOTYPE LOCAL DEFAULT 21 __init_array_start 43: 000000000000201c 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR 44: 0000000000003fb8 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_ 45: 0000000000001000 0 FUNC LOCAL DEFAULT 12 _init 46: 0000000000001210 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini 47: 0000000000004010 8 OBJECT GLOBAL DEFAULT 25 my_name 48: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 49: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start 50: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 25 _edata 51: 0000000000001218 0 FUNC GLOBAL HIDDEN 17 _fini 52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5 53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 54: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start 55: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 56: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle 57: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used 58: 00000000000011a0 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init 59: 0000000000004020 0 NOTYPE GLOBAL DEFAULT 26 _end 60: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start 61: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 __bss_start 62: 0000000000001174 30 FUNC GLOBAL DEFAULT 16 main 63: 0000000000001149 43 FUNC GLOBAL DEFAULT 16 say_hello 64: 0000000000004018 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__ 65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 66: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2 在最后位置找到了亲切的 main和say_hello 62: 0000000000001174 30 FUNC GLOBAL DEFAULT 16 main 63: 0000000000001149 43 FUNC GLOBAL DEFAULT 16 say_hello main函数符号对应的数值为0x1174,其类型为FUNC,大小为30字节,对应的代码区索引为16; say_hello函数符号对应数值为0x1149,其类型为FUNC,大小为43字节,对应的代码区索引同为16。 Section Header Table: [16] .text PROGBITS 0000000000001060 00001060 00000000000001b5 0000000000000000 AX 0 0 16 反汇编代码段 在理解了String Table和Symbol Table的作用后,我们通过objdump反汇编来理解一下.text代码段: root@5e3abe332c5a:/home/docker/test4harmony# objdump -j .text -l -C -S app 0000000000001149 <say_hello>: say_hello(): 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 83 ec 10 sub $0x10,%rsp 1155: 48 89 7d f8 mov %rdi,-0x8(%rbp) 1159: 48 8b 45 f8 mov -0x8(%rbp),%rax 115d: 48 89 c6 mov %rax,%rsi 1160: 48 8d 3d 9d 0e 00 00 lea 0xe9d(%rip),%rdi # 2004 <_IO_stdin_used+0x4> 1167: b8 00 00 00 00 mov $0x0,%eax 116c: e8 df fe ff ff callq 1050 <printf@plt> 1171: 90 nop 1172: c9 leaveq 1173: c3 retq 0000000000001174 <main>: main(): 1174: f3 0f 1e fa endbr64 1178: 55 push %rbp 1179: 48 89 e5 mov %rsp,%rbp 117c: 48 8b 05 8d 2e 00 00 mov 0x2e8d(%rip),%rax # 4010 <my_name> 1183: 48 89 c7 mov %rax,%rdi 1186: e8 be ff ff ff callq 1149 <say_hello> 118b: b8 00 00 00 00 mov $0x0,%eax 1190: 5d pop %rbp 1191: c3 retq 1192: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 1199: 00 00 00 119c: 0f 1f 40 00 nopl 0x0(%rax) 0x1149 0x1174正是say_hello,main函数的入口地址.我们看到了激动人心的调用1149位置的say_hello指令 1186: e8 be ff ff ff callq 1149 <say_hello> 看了这么久还记得开头的C代码的样子吗? 再看一遍 : ) #include <stdio.h> void say_hello(char *who) { printf("hello, %s!\n", who); } char *my_name = "harmony os"; int main() { say_hello(my_name); return 0; } root@5e3abe332c5a:/home/docker/test4harmony# ./app hello, harmony os! 但是!!! 晕,怎么还有but,西卡西...还有完没完.上面让大家记住的还有一个地方没说到 Entry point address: 0x1060 //代码段 .text 起始位置,即程序运行开始位置 它的地址并不是main函数位置0x1174,是0x1060!而且代码段的开始位置是0x1060没错. [16] .text PROGBITS 0000000000001060 00001060 00000000000001b5 0000000000000000 AX 0 0 16 难度main不是入口地址? 那0x1060上放的是何方神圣,再查符号表发现是 60: 0000000000001060 47 FUNC GLOBAL DEFAULT 16 _start 从反汇编堆中找到 _start 0000000000001060 <_start>: _start(): 1060: f3 0f 1e fa endbr64 1064: 31 ed xor %ebp,%ebp 1066: 49 89 d1 mov %rdx,%r9 1069: 5e pop %rsi 106a: 48 89 e2 mov %rsp,%rdx 106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 1071: 50 push %rax 1072: 54 push %rsp 1073: 4c 8d 05 96 01 00 00 lea 0x196(%rip),%r8 # 1210 <__libc_csu_fini> 107a: 48 8d 0d 1f 01 00 00 lea 0x11f(%rip),%rcx # 11a0 <__libc_csu_init> 1081: 48 8d 3d ec 00 00 00 lea 0xec(%rip),%rdi # 1174 <main> 1088: ff 15 52 2f 00 00 callq *0x2f52(%rip) # 3fe0 <__libc_start_main@GLIBC_2.2.5> 108e: f4 hlt 108f: 90 nop 这才看到了亲切的0x1174的main函数.所以结论是: 从内核动态加载的视角看,程序运行首个函数并不是main,而是_start. 但从应用程序开发者视角看,main就是启动函数. 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v51.xx (ELF反汇编篇) | 程序的入口函数并不是main < csdn | 51cto | osc > v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | osc > v49.xx (信号消费篇) | 两次切换用户栈和内核栈 < csdn | 51cto | osc > v48.xx (信号生产篇) | 如何安装和发送异步信号 < csdn | 51cto | osc > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | osc > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | osc > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 < csdn | 51cto | osc > v44.xx (中断管理篇) | 硬中断的实现像观察者模式 < csdn | 51cto | osc > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | osc > v42.xx (中断切换篇) | 中断切换又在切换什么 < csdn | 51cto | osc > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | osc > v40.xx (汇编汇总篇) | 所有的汇编代码都在呢 < csdn | 51cto | osc > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | osc > v38.xx (寄存器篇) | 看完再也不对寄存器怕怕 < csdn | 51cto | osc > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | osc > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆 < csdn | 51cto | osc > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | osc > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航 < csdn | 51cto | osc > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 < csdn | 51cto | osc > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | osc > v31.xx (定时器篇) | 内核最高优先级任务是谁 < csdn | 51cto | osc > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | osc > v29.xx (信号量篇) | 信号量解决任务的什么问题 < csdn | 51cto | osc > v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | osc > v27.xx (互斥锁篇) | 比自旋锁丰满的互斥锁 < csdn | 51cto | osc > v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊 < csdn | 51cto | osc > v25.xx (并发并行篇) | 两个字永远记住二者区别 < csdn | 51cto | osc > v24.xx (进程概念篇) | 进程在管理哪些资源 < csdn | 51cto | osc > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数 < csdn | 51cto | osc > v22.xx (汇编基础篇) | cpu在哪里打卡上班 < csdn | 51cto | osc > v21.xx (线程概念篇) | 是谁在不断的折腾cpu < csdn | 51cto | osc > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | osc > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个 < csdn | 51cto | osc > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | osc > v17.xx (物理内存篇) | 怎么管理物理内存 < csdn | 51cto | osc > v16.xx (内存规则篇) | 内存管理到底在管什么 < csdn | 51cto | osc > v15.xx (内存映射篇) | 虚拟内存虚在哪里 < csdn | 51cto | osc > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础 < csdn | 51cto | osc > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | osc > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的 < csdn | 51cto | osc > v11.xx (内存分配篇) | 内存有哪些分配方式 < csdn | 51cto | osc > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处 < csdn | 51cto | osc > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | osc > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | osc > v07.xx (调度机制篇) | 任务是如何被调度执行的 < csdn | 51cto | osc > v06.xx (调度队列篇) | 内核有多少个调度队列 < csdn | 51cto | osc > v05.xx (任务管理篇) | 任务池是如何管理的 < csdn | 51cto | osc > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | osc > v03.xx (时钟任务篇) | 谁是触发调度的最大动力 < csdn | 51cto | osc > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | osc > v01.xx (双向链表篇) | 谁是内核最重要结构体 < csdn | 51cto | osc > 进入 >> osc | csdn | 51cto | 掘金 | 公众号 | 头条号 | gitee | github 各大站点搜 "鸿蒙内核源码分析" .原创不易,欢迎转载,但请注明出处. 热爱是所有的理由和答案 - turing

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号消费篇) | 谁让CPU连换四次栈运行 | 百篇博客分析HarmonyOS源码 | v49.04

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< osc | 51cto | csdn | harmony > 信号消费 本篇为信号消费篇,读之前建议先阅读信号生产篇,信号部分姊妹篇如下: v48.xx (信号生产篇) | 信号它老人家已经五十多岁了 < csdn | 51cto | harmony > v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > 本篇有相当的难度,涉及用户栈和内核栈的两次切换,寄存器改值,将围绕下图来说明. 解读 从图中可以看出信号消费让CPU四次换栈运行. 为本篇理解方便,把图做简化标签说明: user:用户空间 kernel:内核空间 source(...):源函数 sighandle(...):信号处理函数, syscall(...):系统调用,参数为系统调用号,如sigreturn,N(表任意) user.source():表示在用户空间运行的源函数 系列篇已多次说过,用户态的任务有两个运行栈,一个是用户栈,一个是内核栈.栈空间分别来自用户空间和内核空间.两种空间是有严格的地址划分的,通过虚拟地址的大小就能判断出是用户空间还是内核空间.系统调用本质上是软中断,它使CPU执行指令的场地由用户栈变成内核栈.怎么变的并不复杂,就是改变(sp和cpsr寄存器的值).sp指向哪个栈就代表在哪个栈运行, 当cpu在用户栈运行时是不能访问内核空间的,但内核态任务可以访问整个空间,而且内核态任务没有用户栈. 理解了上面的说明,再来说下正常系统调用流程是这样的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()继续运行,就必须保存用户栈现场各寄存器的值.这些值保存在内核栈中,恢复也是从内核栈恢复. 信号消费的过程的上图可简化表示为: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中间插入了信号处理函数的调用. 这正是本篇要通过代码来说清楚的核心问题. 顺着这个思路可以推到以下几点,实际也是这么做的: kernel.syscall(N) 中必须要再次保存user.source()的上下文sig_switch_context,为何已经保存了一次还要再保存一次? 因为第一次是保存在内核栈中,而内核栈这部分数据会因回到用户态user.sighandle()运行而被恢复现场出栈了.保存现场/恢复现场是成双出队的好基友,注意有些文章说会把整个内核栈清空,这是不对的. 第二次保存在任务结构体中,任务来源于任务池,是内核全局变量,常驻内存的.两次保存的都是user.source()运行时现场信息,再回顾下相关的结构体.关键是sig_switch_context typedef struct { // ... sig_cb sig;//信号控制块,用于异步通信 } LosTaskCB; typedef struct {//信号控制块(描述符) sigset_t sigFlag; //不屏蔽的信号集 sigset_t sigPendFlag; //信号阻塞标签集,记录那些信号来过,任务依然阻塞的集合.即:这些信号不能唤醒任务 sigset_t sigprocmask; /* Signals that are blocked */ //任务屏蔽了哪些信号 sq_queue_t sigactionq; //信号捕捉队列 LOS_DL_LIST waitList; //等待链表,上面挂的是等待信号到来的任务, 请查找 OsTaskWait(&sigcb->waitList, timeout, TRUE) 理解 sigset_t sigwaitmask; /* Waiting for pending signals */ //任务在等待哪些信号的到来 siginfo_t sigunbinfo; /* Signal info when task unblocked */ //任务解锁时的信号信息 sig_switch_context context; //信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换 } sig_cb; 还必须要改变原有PC/R0/R1寄存器的值.想要执行user.sighandle(),PC寄存器就必须指向它,而R0,R1就是它的参数. 信号处理完成后须回到内核态,怎么再次陷入内核态? 答案是:__NR_sigreturn,这也是个系统调用.回来后还原sig_switch_context,即还原user.source()被打断时SP/PC等寄存器的值,使其跳回到用户栈从user.source()的被打断处继续执行. 有了这三个推论,再理解下面的代码就是吹灰之力了,涉及三个关键函数 OsArmA32SyscallHandle,OsSaveSignalContext,OsRestorSignalContext本篇一一解读,彻底挖透.先看信号上下文结构体sig_switch_context. sig_switch_context //任务中断上下文 #define TASK_IRQ_CONTEXT \ unsigned int R0; \ unsigned int R1; \ unsigned int R2; \ unsigned int R3; \ unsigned int R12; \ unsigned int USP; \ unsigned int ULR; \ unsigned int CPSR; \ unsigned int PC; typedef struct {//信号切换上下文 TASK_IRQ_CONTEXT unsigned int R7; //存放系统调用的ID unsigned int count; //记录是否保存了信号上下文 } sig_switch_context; 保存user.source()现场的结构体,USP,ULR代表用户栈指针和返回地址. CPSR寄存器用于设置CPU的工作模式,CPU有7种工作模式,具体可前往翻看 v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > 谈论的用户态(usr普通用户)和内核态(sys超级用户)对应的只是其中的两种.二者都共用相同的寄存器.还原它就是告诉CPU内核已切到普通用户模式运行. 其他寄存器没有保存的原因是系统调用不会用到它们,所以不需要保存. R7是在系统调用发生时用于记录系统调用号,在信号处理过程中,R0将获得信号编号,作为user.sighandle()的第一个参数. count记录是否保存了信号上下文 OsArmA32SyscallHandle 系统调用总入口 /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */ /****************************************************************** 由汇编调用,见于 los_hw_exc.s / BLX OsArmA32SyscallHandle SYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号 regs:参数就是所有寄存器 注意:本函数在用户态和内核态下都可能被调用到 //MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 ******************************************************************/ LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用 if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break; case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//如果有信号要处理,将改写pc,r0,r1寄存器,改变返回正常用户态路径,而先去执行信号处理程序. /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with the SYS_context_switch system call. */ return regs;//返回寄存器的值 } 解读 这是系统调用的总入口,所有的系统调用都要跑这里要统一处理.通过系统号(保存在R7),找到注册函数并回调.完成系统调用过程. 关于系统调用可查看 v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > 本篇不详细说系统调用过程,只说跟信号相关的部分. OsArmA32SyscallHandle总体理解起来是被信号的保存和还原两个函数给包夹了.注意要在运行过程中去理解调用两个函数的过程,对于同一个任务来说,一定是先执行OsSaveSignalContext,第二次进入OsArmA32SyscallHandle后再执行OsRestorSignalContext. 看OsSaveSignalContext,由它负责保存user.source() 的上下文,其中改变了sp,r0/r1寄存器值,切到信号处理函数user.sighandle()运行. 在函数的开头,碰到系统调用号__NR_sigreturn,直接恢复信号上下文就退出了,因为这是要切回user.source()继续运行的操作. //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } OsSaveSignalContext 保存信号上下文 有了上面的铺垫,就不难理解这个函数的作用. /********************************************** 产生系统调用时,也就是软中断时,保存用户栈寄存器现场信息 改写PC寄存器的值 **********************************************/ void OsSaveSignalContext(unsigned int *sp) { UINTPTR sigHandler; UINT32 intSave; LosTaskCB *task = NULL; LosProcessCB *process = NULL; sig_cb *sigcb = NULL; unsigned long cpsr; OS_RETURN_IF_VOID(sp == NULL); cpsr = OS_SYSCALL_GET_CPSR(sp);//获取系统调用时的 CPSR值 OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必须工作在CPU的用户模式下,注意CPSR_USER_MODE(cpu层面)和OS_USER_MODE(系统层面)是两码事. SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信号分发/信号处理篇 task = OsCurrTaskGet(); process = OsCurrProcessGet(); sigcb = &task->sig;//获取任务的信号控制块 //1.未保存任务上下文任务 //2.任何的信号标签集不为空或者进程有信号要处理 if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) { sigHandler = OsGetSigHandler();//获取信号处理函数 if (sigHandler == 0) {//信号没有注册 sigcb->sigFlag = 0; process->sigShare = 0; SCHEDULER_UNLOCK(intSave); PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID); return; } /* One pthread do the share signal */ sigcb->sigFlag |= process->sigShare;//扩展任务的信号标签集 unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1; OsProcessExitCodeSignalSet(process, signo);//设置进程退出信号 sigcb->context.CPSR = cpsr; //保存状态寄存器 sigcb->context.PC = sp[REG_PC]; //获取被打断现场寄存器的值 sigcb->context.USP = sp[REG_SP];//用户栈顶位置,以便能从内核栈切回用户栈 sigcb->context.ULR = sp[REG_LR];//用户栈返回地址 sigcb->context.R0 = sp[REG_R0]; //系统调用的返回值 sigcb->context.R1 = sp[REG_R1]; sigcb->context.R2 = sp[REG_R2]; sigcb->context.R3 = sp[REG_R3]; sigcb->context.R7 = sp[REG_R7];//为何参数不用传R7,是因为系统调用发生时 R7始终保存的是系统调用号. sigcb->context.R12 = sp[REG_R12];//详见 https://my.oschina.net/weharmony/blog/4967613 sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 /* sig No bits 00000100 present sig No 3, but 1<< 3 = 00001000, so signo needs minus 1 */ sigcb->sigFlag ^= 1ULL << (signo - 1); sigcb->context.count++; //代表已保存 } SCHEDULER_UNLOCK(intSave); } 解读 先是判断执行条件,确实是有信号需要处理,有处理函数.自定义处理函数是由用户进程安装进来的,所有进程旗下的任务都共用,参数就是信号signo,注意可不是系统调用号,有区别的.信号编号长这样. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 系统调用号长这样,是不是看到一些很熟悉的函数. #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 #define __NR_lchown 16 #define __NR_break 17 最后是最最最关键的代码,改变pc寄存器的值,此值一变,在_osExceptSwiHdl中恢复上下文后,cpu跳到用户空间的代码段 user.sighandle(R0,R1) 开始执行,即执行信号处理函数. sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 OsRestorSignalContext 恢复信号上下文 /**************************************************** 恢复信号上下文,由系统调用之__NR_sigreturn产生,这是一个内部产生的系统调用. 为什么要恢复呢? 因为系统调用的执行由任务内核态完成,使用的栈也是内核栈,CPU相关寄存器记录的都是内核栈的内容, 而系统调用完成后,需返回任务的用户栈执行,这时需将CPU各寄存器回到用户态现场 所以函数的功能就变成了还原寄存器的值 ****************************************************/ void OsRestorSignalContext(unsigned int *sp) { LosTaskCB *task = NULL; /* Do not adjust this statement */ LosProcessCB *process = NULL; sig_cb *sigcb = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); task = OsCurrTaskGet(); sigcb = &task->sig;//获取当前任务信号控制块 if (sigcb->context.count != 1) {//必须之前保存过,才能被恢复 SCHEDULER_UNLOCK(intSave); PRINT_ERR("sig error count : %d\n", sigcb->context.count); return; } process = OsCurrProcessGet();//获取当前进程 sp[REG_PC] = sigcb->context.PC;//指令寄存器 OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序状态寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 sp[REG_R0] = sigcb->context.R0; sp[REG_R1] = sigcb->context.R1; sp[REG_R2] = sigcb->context.R2; sp[REG_R3] = sigcb->context.R3; sp[REG_R7] = sigcb->context.R7; sp[REG_R12] = sigcb->context.R12; sigcb->context.count--; //信号上下文的数量回到减少 process->sigShare = 0; //回到用户态,信号共享清0 OsProcessExitCodeSignalClear(process);//清空进程退出码 SCHEDULER_UNLOCK(intSave); } 解读 在信号处理函数完成之后,内核会触发一个__NR_sigreturn的系统调用,又陷入内核态,回到了OsArmA32SyscallHandle. 恢复的过程很简单,把之前保存的信号上下文恢复到内核栈sp开始位置,数据在栈中的保存顺序可查看 用栈方式篇 ,最重要的看这几句. sp[REG_PC] = sigcb->context.PC;//指令寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USP和ULR指向的是用户栈的位置.一旦PC,USP,ULR从栈中弹出赋给寄存器.才真正完成了内核栈到用户栈的切换.回到了user.source()继续运行. 真正的切换汇编代码如下,都已添加注释,在保存和恢复上下文中夹着OsArmA32SyscallHandle @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理,注意此时已在内核栈运行 @保存任务上下文(TaskContext) 开始... 一定要对照TaskContext来理解 SUB SP, SP, #(4 * 16) @先申请16个栈空间单元用于处理本次软中断 STMIA SP, {R0-R12} @TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP SUB SP, SP, #4 @ => TaskContext.resved PUSH_FPU_REGS R1 @保存中断模式(用户模式) @保存任务上下文(TaskContext) 结束 MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/ CPSID I @执行后续指令前必须先关中断 @恢复任务上下文(TaskContext) 开始 POP_FPU_REGS R1 @弹出FPU值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 @恢复任务上下文(TaskContext) 结束 具体也可看这两篇: v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v51.xx (ELF反汇编篇) | 程序的入口函数并不是main < csdn | 51cto | osc > v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | osc > v49.xx (信号消费篇) | 谁让CPU连换四次栈运行 < csdn | 51cto | osc > v48.xx (信号生产篇) | 信号它老人家已经五十多岁了 < csdn | 51cto | osc > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | osc > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | osc > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 < csdn | 51cto | osc > v44.xx (中断管理篇) | 硬中断的实现像观察者模式 < csdn | 51cto | osc > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | osc > v42.xx (中断切换篇) | 中断切换又在切换什么 < csdn | 51cto | osc > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | osc > v40.xx (汇编汇总篇) | 所有的汇编代码都在呢 < csdn | 51cto | osc > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | osc > v38.xx (寄存器篇) | 看完再也不对寄存器怕怕 < csdn | 51cto | osc > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | osc > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆 < csdn | 51cto | osc > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | osc > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航 < csdn | 51cto | osc > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 < csdn | 51cto | osc > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | osc > v31.xx (定时器篇) | 内核最高优先级任务是谁 < csdn | 51cto | osc > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | osc > v29.xx (信号量篇) | 信号量解决任务的什么问题 < csdn | 51cto | osc > v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | osc > v27.xx (互斥锁篇) | 比自旋锁丰满的互斥锁 < csdn | 51cto | osc > v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊 < csdn | 51cto | osc > v25.xx (并发并行篇) | 两个字永远记住二者区别 < csdn | 51cto | osc > v24.xx (进程概念篇) | 进程在管理哪些资源 < csdn | 51cto | osc > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数 < csdn | 51cto | osc > v22.xx (汇编基础篇) | cpu在哪里打卡上班 < csdn | 51cto | osc > v21.xx (线程概念篇) | 是谁在不断的折腾cpu < csdn | 51cto | osc > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | osc > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个 < csdn | 51cto | osc > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | osc > v17.xx (物理内存篇) | 怎么管理物理内存 < csdn | 51cto | osc > v16.xx (内存规则篇) | 内存管理到底在管什么 < csdn | 51cto | osc > v15.xx (内存映射篇) | 虚拟内存虚在哪里 < csdn | 51cto | osc > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础 < csdn | 51cto | osc > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | osc > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的 < csdn | 51cto | osc > v11.xx (内存分配篇) | 内存有哪些分配方式 < csdn | 51cto | osc > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处 < csdn | 51cto | osc > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | osc > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | osc > v07.xx (调度机制篇) | 任务是如何被调度执行的 < csdn | 51cto | osc > v06.xx (调度队列篇) | 内核有多少个调度队列 < csdn | 51cto | osc > v05.xx (任务管理篇) | 任务池是如何管理的 < csdn | 51cto | osc > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | osc > v03.xx (时钟任务篇) | 谁是触发调度的最大动力 < csdn | 51cto | osc > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | osc > v01.xx (双向链表篇) | 谁是内核最重要结构体 < csdn | 51cto | osc > 进入 >> osc | csdn | 51cto | 掘金 | 公众号 | 头条号 | gitee | github 鸿蒙内核源码分析系列篇.原创不易,欢迎转载,但请注明出处. 热爱是所有的理由和答案 - turing 不经过深度思考的人生不值得一过 - turing

优秀的个人博客,低调大师

| 百篇博客分析HarmonyOS源码 | v48.01

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号 关于信号篇,本来只想写一篇,但发现把它想简单了,内容不多,难度极大.看完鸿蒙这部分源码后理解了为何<<深入理解linux内核>>要单独为它开一章. 1.是信号相关的结构体多,而且还容易搞混.所以看本篇要注意结构体的名字和作用. 2.是系统调用太多了,涉及面广,信号的来源分硬件和软件.相当于软中断和硬中断,信号的处理就会涉及汇编代码,运行过程需要在用户空间和内核空间来回的捯饬,频繁的切换任务上下文和栈空间. 本篇试图将任务和信号的关系比成男孩和女孩互追的过程.希望能把问题简化说明白,爱我的人和我爱的人这是永恒的话题.确实难搞. 所以分成信号分发和信号处理两篇: 本篇是信号分发,说概念,说结构体,理清信号和进程,任务的关系.最后说发送过程. 另外一篇专门说信号处理过程,围绕信号切换上下文sig_switch_context来说.是会很有意思的一篇. 信号,为系统提供了一种处理异步事件的方法,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不知道信号到底什么时候到达。一般来说,我们只需要在进程中设置信号相应的处理函数,当有信号到达的时候,由内核空间异步回调用户空间的信号处理函数即可。 信号分类 每个信号都有一个名字和编号,这些名字都以SIG开头,例如SIGQUIT、SIGCHLD等等。 信号定义在signal.h头文件中,信号名都定义为正整数。 具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。不过kill对于信号0有特殊的应用。啥用呢? 可用来查询进程是否还在. 敲下 kill 0 pid 就知道了. 信号分为两大类:可靠信号与不可靠信号,前32种信号为不可靠信号,后32种为可靠信号。 不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31; 可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64 #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //终止信号 #define SIGSTKFLT 16 //栈溢出 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 | 不能被忽略、处理和阻塞 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 #define SIGTTIN 21 //进程停止,后台进程企图从控制终端读 #define SIGTTOU 22 //进程停止,后台进程企图从控制终端写 #define SIGURG 23 //I/O有紧急数据到达当前进程 #define SIGXCPU 24 //进程的CPU时间片到期 #define SIGXFSZ 25 //文件大小的超出上限 #define SIGVTALRM 26 //虚拟时钟超时 #define SIGPROF 27 //profile时钟超时 #define SIGWINCH 28 //窗口大小改变 #define SIGIO 29 //I/O相关 #define SIGPOLL 29 // #define SIGPWR 30 //电源故障,关机 #define SIGSYS 31 //系统调用中参数错,如系统调用号非法 #define SIGUNUSED SIGSYS//不使用 #define _NSIG 65 信号产生 信号来源分为硬件类和软件类: 硬件类 用户输入:比如在终端上按下组合键ctrl+C,产生SIGINT信号; 硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程; 软件类 通过系统调用,发送signal信号:kill(),raise(),sigqueue(),alarm(),setitimer(),abort() kill 命令就是一个发送信号的工具,用于向进程或进程组发送信号.例如: kill 9 PID (SIGKILL)来杀死PID 进程. sigqueue():只能向一个进程发送信号,不能像进程组发送信号;主要针对实时信号提出,与sigaction()组合使用,当然也支持非实时信号的发送; alarm():用于调用进程指定时间后发出SIGALARM信号; setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大; abort():向进程发送SIGABORT信号,默认进程会异常退出。 raise():用于向进程自身发送信号; 信号与进程相关结构体 typedef struct ProcessCB {//PCB中关于信号的信息 UINTPTR sigHandler; /**< signal handler */ //捕捉信号后的处理函数 sigset_t sigShare; /**< signal share bit */ //信号共享位 }LosProcessCB; typedef unsigned _Int64 sigset_t; //一个64位的变量,每个信号代表一位. struct sigaction {//信号处理机制结构体 union { void (*sa_handler)(int); //信号处理函数——普通版 void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理函数——高级版 } __sa_handler; sigset_t sa_mask;//指定信号处理程序执行过程中需要阻塞的信号; int sa_flags; //标示位 // SA_RESTART:使被信号打断的syscall重新发起。 // SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。 // SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵 尸进程。 // SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。 // SA_RESETHAND:信号处理之后重新设置为默认的处理方式。 // SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。 void (*sa_restorer)(void); }; typedef struct sigaction sigaction_t; 解读 每个信号都对应一个位. 信号从1开始编号 [1 ~ 64] 对应 sigShare的[0 ~ 63]位,所以中间会差一个.记住这点,后续代码会提到. sigHandler信号处理函数的设置过程,由系统调用sigaction(用户空间) -> OsSigAction(内核空间)得到 #include <signal.h> int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); int OsSigAction(int sig, const sigaction_t *act, sigaction_t *oact) { UINTPTR addr; sigaction_t action; if (!GOOD_SIGNO(sig) || sig < 1 || act == NULL) { return -EINVAL; } //将数据从用户空间拷贝到内核空间 if (LOS_ArchCopyFromUser(&action, act, sizeof(sigaction_t)) != LOS_OK) { return -EFAULT; } if (sig == SIGSYS) {//系统调用中参数错,如系统调用号非法 addr = OsGetSigHandler();//获取进程信号处理函数 if (addr == 0) {//进程没有设置信号处理函数时 OsSetSigHandler((unsigned long)(UINTPTR)action.sa_handler);//设置进程信号处理函数——普通版 return LOS_OK; } return -EINVAL; } return LOS_OK; } sigaction()第一个参数是要捕捉的信号; 第二个参数与sigaction函数同名的结构体, 结构体内定义了信号处理方法;第三个为输出参数,将信号的当前的sigaction结构地址输出.但鸿蒙显然没有认真对待第三个参数. sigaction()可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体. 鸿蒙目前支持信号处理函数——普通版,sa_handler表示自定义函数捕捉信号,或者说用户进程向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,具体看注释 sa_sigaction是实时信号的处理函数,union二选一.鸿蒙暂时不支持这种方式. 信号与任务相关结构体 typedef struct {//TCB中关于信号的信息 sig_cb sig; //信号控制块,用于异步通信,类似于 linux singal模块 } LosTaskCB; typedef struct {//信号控制块(描述符) sigset_t sigFlag; //不屏蔽的信号标签集 sigset_t sigPendFlag; //信号阻塞标签集,记录因哪些信号被阻塞 sigset_t sigprocmask; /* Signals that are blocked */ //进程屏蔽了哪些信号 sq_queue_t sigactionq; //信号捕捉队列 LOS_DL_LIST waitList; //等待链表,上面挂的是等待信号到来的任务, 可查找 OsTaskWait(&sigcb->waitList, timeout, TRUE) 理解 sigset_t sigwaitmask; /* Waiting for pending signals */ //任务在等待阻塞信号 siginfo_t sigunbinfo; /* Signal info when task unblocked */ //任务解锁时的信号信息 sig_switch_context context; //信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换 } sig_cb; 解读 系列篇已多次说过,进程只是管理资源的容器,真正让cpu干活的是任务task,所以发给进程的信号最终还是需要分发给具体任务来处理.所以能想到的是关于任务部分会更复杂. context信号处理很复杂的原因在于信号的发起在用户空间,发送需要系统调用,而处理信号的函数又是用户空间提供的, 所以需要反复的切换任务上下文.而且还有硬中断的问题,比如 ctrl + c ,需要从硬中断中回调用户空间的信号处理函数,处理完了再回到内核空间,最后回到用户空间.没听懂吧,我自己都说晕了,所以需要专门的一篇来说清楚信号的处理问题.本篇不展开说. sig_cb结构体是任务处理信号的结构体,要响应,屏蔽哪些信号等等都由它完成,这个结构体虽不复杂,但是很绕,很难搞清楚它们之间的区别.笔者是经过一番痛苦的阅读理解后才明白各自的含义.并想通过用打比方的例子试图让大家明白. 用追女孩子这种永恒的爱情话题来理解吧.任务相当于某个男孩子,没错说的就是屏幕前的你,除了我们这些苦逼的码农谁会有耐心看到这里.信号就是女孩子.理解如下: waitList等待信号的任务链表,上面挂的是因等待信号而被阻塞的任务,都在排队追心爱的女孩,处于无所事事的挂起的状态,二流子,等待猎物们的出现. sigwaitmask任务在等待的信号集合,只有这些信号能唤醒任务.相当于你爱慕的诸多女孩,只要出现一位就能让你满血复活. sigprocmask指任务对哪些信号不感冒.来了也不处理.相当于你明确告知了哪些女孩不喜欢. sigPendFlag信号到大但并未唤醒任务.相当于爱你的女孩来找你了,但你不搭理人家继续挂起,你这得多贱的. sigFlag记录不屏蔽的信号集合,相当于你并不反感的女孩们,不说喜欢也不说不喜欢.渣男! 信号发送过程 用户进程调用kill()的过程如下: kill(pid_t pid, int sig) - 系统调用 | 用户空间 --------------------------------------------------------------------------------------- | 内核空间 SysKill(...) |---> OsKillLock(...) |---> OsKill(.., OS_USER_KILL_PERMISSION) |---> OsDispatch() //鉴权,向进程发送信号 |---> OsSigProcessSend() //选择任务发送信号 |---> OsSigProcessForeachChild(..,ForEachTaskCB handler,..) |---> SigProcessKillSigHandler() //处理 SIGKILL |---> OsTaskWake() //唤醒所有等待任务 |---> OsSigEmptySet() //清空信号等待集 |---> SigProcessSignalHandler() |---> OsTcbDispatch() //向目标任务发送信号 |---> OsTaskWake() //唤醒任务 |---> OsSigEmptySet() //清空信号等待集 流程 通过 系统调用 kill 陷入内核空间 因为是用户态进程,使用OS_USER_KILL_PERMISSION权限发送信号 #define OS_KERNEL_KILL_PERMISSION 0U //内核态 kill 权限 #define OS_USER_KILL_PERMISSION 3U //用户态 kill 权限 鉴权之后进程轮询任务组,向目标任务发送信号.这里分三种情况: SIGKILL信号,将所有等待任务唤醒,拉入就绪队列等待被调度执行,并情况信号等待集 非SIGKILL信号时,将通过sigwaitmask和sigprocmask过滤,找到一个任务向它发送信号OsTcbDispatch. 代码细节 int OsKill(pid_t pid, int sig, int permission) { siginfo_t info; int ret; /* Make sure that the para is valid */ if (!GOOD_SIGNO(sig) || pid < 0) {//有效信号 [0,64] return -EINVAL; } if (OsProcessIDUserCheckInvalid(pid)) {//检查参数进程 return -ESRCH; } /* Create the siginfo structure */ //创建信号结构体 info.si_signo = sig; //信号编号 info.si_code = SI_USER; //来自用户进程信号 info.si_value.sival_ptr = NULL; /* Send the signal */ ret = OsDispatch(pid, &info, permission);//发送信号 return ret; } OsDispatch() 函数的代码如下: //信号分发 int OsDispatch(pid_t pid, siginfo_t *info, int permission) { LosProcessCB *spcb = OS_PCB_FROM_PID(pid);//找到这个进程 if (OsProcessIsUnused(spcb)) {//进程是否还在使用,不一定是当前进程但必须是个有效进程 return -ESRCH; } #ifdef LOSCFG_SECURITY_CAPABILITY //启用能力安全模式 LosProcessCB *current = OsCurrProcessGet();//获取当前进程 /* If the process you want to kill had been inactive, but still exist. should return LOS_OK */ if (OsProcessIsInactive(spcb)) {//如果要终止的进程处于非活动状态,但仍然存在,应该返回OK return LOS_OK; } /* Kernel process always has kill permission and user process should check permission *///内核进程总是有kill权限,用户进程需要检查权限 if (OsProcessIsUserMode(current) && !(current->processStatus & OS_PROCESS_FLAG_EXIT)) {//用户进程检查能力范围 if ((current != spcb) && (!IsCapPermit(CAP_KILL)) && (current->user->userID != spcb->user->userID)) { return -EPERM; } } #endif if ((permission == OS_USER_KILL_PERMISSION) && (OsSignalPermissionToCheck(spcb) < 0)) { return -EPERM; } return OsSigProcessSend(spcb, info);//给参数进程发送信号 } //给参数进程发送参数信号 int OsSigProcessSend(LosProcessCB *spcb, siginfo_t *sigInfo) { int ret; struct ProcessSignalInfo info = { .sigInfo = sigInfo, //信号内容 .defaultTcb = NULL, //以下四个值将在OsSigProcessForeachChild中根据条件完善 .unblockedTcb = NULL, .awakenedTcb = NULL, .receivedTcb = NULL }; //总之是要从进程中找个至少一个任务来接受这个信号,优先级 //awakenedTcb > receivedTcb > unblockedTcb > defaultTcb /* visit all taskcb and dispatch signal */ //访问所有任务和分发信号 if ((info.sigInfo != NULL) && (info.sigInfo->si_signo == SIGKILL)) {//需要干掉进程时 SIGKILL = 9, #linux kill 9 14 (void)OsSigProcessForeachChild(spcb, SigProcessKillSigHandler, &info);//进程要被干掉了,通知所有task做善后处理 OsSigAddSet(&spcb->sigShare, info.sigInfo->si_signo); OsWaitSignalToWakeProcess(spcb);//等待信号唤醒进程 return 0; } else { ret = OsSigProcessForeachChild(spcb, SigProcessSignalHandler, &info);//进程通知所有task处理信号 } if (ret < 0) { return ret; } SigProcessLoadTcb(&info, sigInfo); return 0; } //让进程的每一个task执行参数函数 int OsSigProcessForeachChild(LosProcessCB *spcb, ForEachTaskCB handler, void *arg) { int ret; /* Visit the main thread last (if present) */ LosTaskCB *taskCB = NULL;//遍历进程的 threadList 链表,里面存放的都是task节点 LOS_DL_LIST_FOR_EACH_ENTRY(taskCB, &(spcb->threadSiblingList), LosTaskCB, threadList) {//遍历进程的任务列表 ret = handler(taskCB, arg);//回调参数函数 OS_RETURN_IF(ret != 0, ret);//这个宏的意思就是只有ret = 0时,啥也不处理.其余就返回 ret } return LOS_OK; } 解读 OsSigProcessSend 如果是 SIGKILL信号,让spcb的所有任务执行SigProcessKillSigHandler函数,查看旗下的所有任务是否又在等待这个信号的,如果有就将任务唤醒,放在就绪队列等待被调度执行. //进程收到 SIGKILL 信号后,通知任务tcb处理. static int SigProcessKillSigHandler(LosTaskCB *tcb, void *arg) { struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//转参 if ((tcb != NULL) && (info != NULL) && (info->sigInfo != NULL)) {//进程有信号 sig_cb *sigcb = &tcb->sig; if (!LOS_ListEmpty(&sigcb->waitList) && OsSigIsMember(&sigcb->sigwaitmask, info->sigInfo->si_signo)) {//如果任务在等待这个信号 OsTaskWake(tcb);//唤醒这个任务,加入进程的就绪队列,并不申请调度 OsSigEmptySet(&sigcb->sigwaitmask);//清空信号等待位,不等任何信号了.因为这是SIGKILL信号 } } return 0; } 非SIGKILL信号,让spcb的所有任务执行SigProcessSignalHandler函数 static int SigProcessSignalHandler(LosTaskCB *tcb, void *arg) { struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//先把参数解出来 int ret; int isMember; if (tcb == NULL) { return 0; } /* If the default tcb is not setted, then set this one as default. */ if (!info->defaultTcb) {//如果没有默认发送方的任务,即默认参数任务. info->defaultTcb = tcb; } isMember = OsSigIsMember(&tcb->sig.sigwaitmask, info->sigInfo->si_signo);//任务是否在等待这个信号 if (isMember && (!info->awakenedTcb)) {//是在等待,并尚未向该任务时发送信号时 /* This means the task is waiting for this signal. Stop looking for it and use this tcb. * The requirement is: if more than one task in this task group is waiting for the signal, * then only one indeterminate task in the group will receive the signal. */ ret = OsTcbDispatch(tcb, info->sigInfo);//发送信号,注意这是给其他任务发送信号,tcb不是当前任务 OS_RETURN_IF(ret < 0, ret);//这种写法很有意思 /* set this tcb as awakenedTcb */ info->awakenedTcb = tcb; OS_RETURN_IF(info->receivedTcb != NULL, SIG_STOP_VISIT); /* Stop search */ } /* Is this signal unblocked on this thread? */ isMember = OsSigIsMember(&tcb->sig.sigprocmask, info->sigInfo->si_signo);//任务是否屏蔽了这个信号 if ((!isMember) && (!info->receivedTcb) && (tcb != info->awakenedTcb)) {//没有屏蔽,有唤醒任务没有接收任务. /* if unblockedTcb of this signal is not setted, then set it. */ if (!info->unblockedTcb) { info->unblockedTcb = tcb; } ret = OsTcbDispatch(tcb, info->sigInfo);//向任务发送信号 OS_RETURN_IF(ret < 0, ret); /* set this tcb as receivedTcb */ info->receivedTcb = tcb;//设置这个任务为接收任务 OS_RETURN_IF(info->awakenedTcb != NULL, SIG_STOP_VISIT); /* Stop search */ } return 0; /* Keep searching */ } 函数的意思是,当进程中有多个任务在等待这个信号时,发送信号给第一个等待的任务awakenedTcb. 如果没有任务在等待信号,那就从不屏蔽这个信号的任务集中随机找一个receivedTcb接受信号. 只要不屏蔽 unblockedTcb就有值,随机的. 如果上面的都不满足,信号发送给defaultTcb. 寻找发送任务的优先级是 awakenedTcb > receivedTcb > unblockedTcb > defaultTcb 信号相关函数 信号集操作函数 sigemptyset(sigset_t *set):信号集全部清0; sigfillset(sigset_t *set): 信号集全部置1,则信号集包含linux支持的64种信号; sigaddset(sigset_t *set, int signum):向信号集中加入signum信号; sigdelset(sigset_t *set, int signum):向信号集中删除signum信号; sigismember(const sigset_t *set, int signum):判定信号signum是否存在信号集中。 信号阻塞函数 sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); 不同how参数,实现不同功能 SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集; SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除; SIG_SETMASK:将set指向信号集中的信号,设置成进程阻塞信号集; sigpending(sigset_t *set)):获取已发送到进程,却被阻塞的所有信号; sigsuspend(const sigset_t *mask)):用mask代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。 鸿蒙源码百篇博客 往期回顾 v48.xx (信号分发篇) | 任务和信号能比喻成什么关系? < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | 51cto | harmony > v36.xx (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 内核是如何描述cpu的? < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处. 进入 >> oschina | csdn | 51cto | 简书 | 掘金 | harmony

优秀的个人博客,低调大师

鸿蒙内核源码分析(进程通讯篇) | 九大通讯方式一网打尽 | 中文注解HarmonyOS源码 | v28.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇开始介绍进程通讯(IPC) 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 进程间为何要通讯 ? 鸿蒙内核默认支持 64个进程 和 128个任务. 内核设计尽量不去打扰它们,让各人过好各自的日子, 但大家毕竟在一口锅里吃饭, 不可能不与外界联系, 联系就得有方法,有规矩. 举两个应用场景说明下通讯的必要性: 一.被动式 广为熟知的shell命令 kill 9 13 ,是通过 shell任务给 13号进程发送一个干掉它的信号. #define SIGKILL 9 //常用的命令 kill 9 13 这是被动式通讯的场景,至于为什么要干掉你,原因可能很多啊,很可能是检测到13占用内存太多了,也可能13太低调长期不活跃,启动新进程发现没位置了,得收了你.反正系统必须得有对付你的抓手. 二.主动式的 ,比如要访问某些公共资源(全局变量,消息队列),而资源有限或具有排他性,别人正在使用导致你不能用, 所以需统一管理,要用就必须要先申请,按规矩办事,毕竟和谐社会没规矩不成方圆. 总之大概有以下几种需求需要通讯: (1).数据传输: 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间 (2).共享数据: 多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。 (3).通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 (4).资源共享: 多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。 (5).进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 内核目录和系列篇更新 内核有个专门的IPC目录,详见如下. 可直接点击查看注解源码. 进程间通讯方式 查看源码注解点击一下 .c 文件 1.管道pipe(fs_syscall.c) 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。 调用pipe系统函数即可创建一个管道。有如下特质: 其本质是一个伪文件(实为内核缓冲区) 由两个文件描述符引用,一个表示读端,一个表示写端。 规定数据从管道的写端流入管道,从读端流出。 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。 管道的局限性: ① 数据自己读不能自己写。 ② 数据一旦被读走,便不在管道中存在,不可反复读取。 ③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。 ④ 只能在有公共祖先的进程间使用管道。 常见的通信方式有,单工通信、半双工通信、全双工通信。 鸿蒙的管道实现很简单, 详细看SysPipe函数. 2.信号(los_signal.c) 信号是用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。 如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程被调度执行并传递给它为止。 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。 进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。 #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(如break键被按下) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 13 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //软件终止(software termination) #define SIGSTKFLT 16 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 3.消息队列(los_queue.c) 基本概念 队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的 不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。 任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时, 挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时, 挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。如果将 读队列和写队列的超时时间设置为0,则不会挂起任务,接口会直接返回,这就是非阻塞模式。 消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用。 队列特性 消息以先进先出的方式排队,支持异步读写。 读队列和写队列都支持超时机制。 每读取一条消息,就会将该消息节点设置为空闲。 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。 一个任务能够从任意一个消息队列接收和发送消息。 多个任务能够从同一个消息队列接收和发送消息。 创建队列时所需的队列空间,默认支持接口内系统自行动态申请内存的方式,同时也支持将用户分配的队列空间作为接口入参传入的方式。 4.共享内存(shm.c) 共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存 不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候, 其它进程都会察觉到这个更改。 (共享内存篇)| 正在更新中... 5.信号量(los_sem.c) 基本概念 信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量, 然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他 需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时, 建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量, 任务1才得以进入Ready或Running态,从而达到了任务间的同步。 使用场景 在多任务系统中,信号量是一种非常灵活的同步方式,可以运用在多种场合中,实现锁、同步、资源计数等功能, 也能方便的用于任务与任务,中断与任务的同步中。信号量常用于协助一组相互竞争的任务访问共享资源。 (信号量篇)|正在更新中... 6.互斥锁 (los_mux.c) : 基本概念 互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。 另外,互斥锁可以解决信号量存在的优先级翻转问题。 任意时刻互斥锁只有两种状态,开锁或闭锁。当任务持有时,这个任务获得该互斥锁的所有权, 互斥锁处于闭锁状态。当该任务释放锁后,任务失去该互斥锁的所有权,互斥锁处于开锁状态。 当一个任务持有互斥锁时,其他任务不能再对该互斥锁进行开锁或持有。 详见: (互斥锁篇) | 为何任务会因拥有锁而被迫改变优先级 ? 7.快锁 (los_futex.c) futex 是Fast Userspace muTexes的缩写(快速用户空间互斥体),是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。 8.事件 (los_event.c) 基本概念 事件(Event)是一种任务间通信的机制,可用于任务间的同步。 多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。 一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。 多对多同步模型:多个任务等待多个事件的触发。 事件特点 任务通过创建事件控制块来触发事件或等待事件。 事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。 事件仅用于任务间的同步,不提供数据传输功能。 多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。 多个任务可以对同一事件进行读写操作。 支持事件读写超时机制。 事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。 使用场景 队列用于任务间通信,可以实现消息的异步处理。同时消息的发送方和接收方不需要彼此联系,两者间是解耦的。 (事件驱动篇)|正在更新中... 9.文件消息队列 (hm_liteipc.c) 基于文件实现的消息队列,特点是队列中消息数量多(256个),传递消息内容大(可达到1K) #define IPC_MSG_DATA_SZ_MAX 1024 //最大的消息内容 1K ,posix最大消息内容 64个字节 #define IPC_MSG_OBJECT_NUM_MAX 256 //最大的消息数量256 ,posix最大消息数量 16个 (消息队列篇)|正在更新中... 以上为鸿蒙内核的九种进程间通讯方式,源码注解已基本完成,可前往Fork查看. 每一种通讯方式博客将详细说明,正在更新中... 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号消费篇) | 用户栈到内核栈的两次切换 | 百篇博客分析HarmonyOS源码 | v49.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号消费 本篇为信号消费篇,读之前建议先阅读信号生产篇,信号部分姊妹篇如下: v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > 本篇有相当的难度,涉及用户栈和内核栈的两次切换,寄存器改值,将围绕下图来说明. 解读 为本篇理解方便,把图做简化标签说明: user:用户空间, kernel:内核空间, source:源函数 sighandle:信号处理函数,syscall:任意系统调用号-N, syscall:信号处理完成的系统调用号-sigreturn 系列篇已多次说过,用户态的任务有两个运行栈,一个是用户栈,一个是内核栈.栈空间分别来自用户空间和内核空间.两种空间是有严格的地址划分的,通过虚拟地址的大小就能判断出是用户空间还是内核空间.系统调用本质上是软中断,它使CPU执行指令的场地由用户栈变成内核栈.怎么变的并不复杂,就是改变(sp和cpsr寄存器的值).sp指向哪个栈就代表在哪个栈运行, 当cpu在用户栈运行时是不能访问内核空间的,但内核态任务可以访问整个空间,而且内核态任务没有用户栈. 理解了上面的说明,再来说下正常系统调用流程是这样的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()继续运行,就必须保存用户栈现场各寄存器的值.这些值保存在内核栈中,恢复也是从内核栈恢复. 信号消费的过程的上图可简化表示为: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中间插入了信号处理函数的调用. 这正是本篇要通过代码来说清楚的核心问题. 顺着这个思路可以推到以下几点,实际也是这么做的: kernel.syscall(N) 中必须要再次保存user.source()的上下文sig_switch_context,为什么已经保存了一遍还要再保存一次? 因为第一次是保存在内核栈中,而内核栈的数据会因回到用户态user.sighandle()运行而被清空数据.第二次保存在任务结构体中,任务来源于任务池,是内核全局变量,常驻内存的. typedef struct { // ... sig_cb sig;//信号控制块,用于异步通信 } LosTaskCB; 还必须要改变原有PC/R0/R1寄存器的值.因为要执行user.sighandle(),PC寄存器就必须指向它,而R0,R1就是它的参数. 信号处理完成后须回到内核态,怎么再次陷入内核态? 答案是:__NR_sigreturn,这也是个系统调用.回来后还原sig_switch_context,即还原user.source()被打断时SP/PC等寄存器的值,使其跳回到用户栈从user.source()打断处继续执行. 有了这三个推论,再理解下面的代码就是吹灰之力了,涉及三个关键函数 OsArmA32SyscallHandle,OsSaveSignalContext,OsRestorSignalContext本篇一一解读,彻底挖透.先看信号上下文结构体sig_switch_context. sig_switch_context //任务中断上下文 #define TASK_IRQ_CONTEXT \ unsigned int R0; \ unsigned int R1; \ unsigned int R2; \ unsigned int R3; \ unsigned int R12; \ unsigned int USP; \ unsigned int ULR; \ unsigned int CPSR; \ unsigned int PC; typedef struct {//信号切换上下文 TASK_IRQ_CONTEXT unsigned int R7; //存放系统调用的ID unsigned int count; //记录是否保存了信号上下文 } sig_switch_context; 这是保存用户栈的结构体,USP,ULR代表用户栈指针和返回地址. 其他寄存器没有保存的原因是系统调用不会用到那些寄存器,所以不需要保存. R7是在系统调用发生时用于记录系统调用号,在信号处理过程中,R0将获得信号编号,作为user.sighandle()的第一个参数. count记录是否保存了信号上下文 OsArmA32SyscallHandle 系统调用总入口 /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */ /****************************************************************** 由汇编调用,见于 los_hw_exc.s / BLX OsArmA32SyscallHandle SYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号 regs:参数就是所有寄存器 注意:本函数在用户态和内核态下都可能被调用到 //MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 ******************************************************************/ LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用 if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break; case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//如果有信号要处理,将改写pc,r0,r1寄存器,改变返回正常用户态路径,而先去执行信号处理程序. /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with the SYS_context_switch system call. */ return regs;//返回寄存器的值 } 解读 这是系统调用的总入口,所有的系统调用都要跑这里要统一处理.通过系统号(保存在R7),找到注册函数并回调.完成系统调用过程. 关于系统调用可查看 v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > 本篇不详细说系统调用过程,只说跟信号相关的部分. OsArmA32SyscallHandle总体理解起来是被信号的保存和还原两个函数给包夹了.注意要在运行过程中去理解调用两个函数的过程,对于同一个任务来说,一定是先执行OsSaveSignalContext,第二次进入OsArmA32SyscallHandle后再执行OsRestorSignalContext. 看OsSaveSignalContext,由它负责保存user.source() 的上下文,其中改变了sp,r0/r1寄存器值,切到信号处理函数user.sighandle()运行. 在函数的开头,碰到系统调用号__NR_sigreturn,直接恢复信号上下文就退出了,因为这是要切回user.source()继续运行的操作. //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } OsSaveSignalContext 保存信号上下文 有了上面的铺垫,就不难理解这个函数的作用. /********************************************** 产生系统调用时,也就是软中断时,保存用户栈寄存器现场信息 改写PC寄存器的值 **********************************************/ void OsSaveSignalContext(unsigned int *sp) { UINTPTR sigHandler; UINT32 intSave; LosTaskCB *task = NULL; LosProcessCB *process = NULL; sig_cb *sigcb = NULL; unsigned long cpsr; OS_RETURN_IF_VOID(sp == NULL); cpsr = OS_SYSCALL_GET_CPSR(sp);//获取系统调用时的 CPSR值 OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必须工作在CPU的用户模式下,注意CPSR_USER_MODE(cpu层面)和OS_USER_MODE(系统层面)是两码事. SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信号分发/信号处理篇 task = OsCurrTaskGet(); process = OsCurrProcessGet(); sigcb = &task->sig;//获取任务的信号控制块 //1.未保存任务上下文任务 //2.任何的信号标签集不为空或者进程有信号要处理 if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) { sigHandler = OsGetSigHandler();//获取信号处理函数 if (sigHandler == 0) {//信号没有注册 sigcb->sigFlag = 0; process->sigShare = 0; SCHEDULER_UNLOCK(intSave); PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID); return; } /* One pthread do the share signal */ sigcb->sigFlag |= process->sigShare;//扩展任务的信号标签集 unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1; OsProcessExitCodeSignalSet(process, signo);//设置进程退出信号 sigcb->context.CPSR = cpsr; //保存状态寄存器 sigcb->context.PC = sp[REG_PC]; //获取被打断现场寄存器的值 sigcb->context.USP = sp[REG_SP];//用户栈顶位置,以便能从内核栈切回用户栈 sigcb->context.ULR = sp[REG_LR];//用户栈返回地址 sigcb->context.R0 = sp[REG_R0]; //系统调用的返回值 sigcb->context.R1 = sp[REG_R1]; sigcb->context.R2 = sp[REG_R2]; sigcb->context.R3 = sp[REG_R3]; sigcb->context.R7 = sp[REG_R7];//为何参数不用传R7,是因为系统调用发生时 R7始终保存的是系统调用号. sigcb->context.R12 = sp[REG_R12];//详见 https://my.oschina.net/weharmony/blog/4967613 sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 /* sig No bits 00000100 present sig No 3, but 1<< 3 = 00001000, so signo needs minus 1 */ sigcb->sigFlag ^= 1ULL << (signo - 1); sigcb->context.count++; //代表已保存 } SCHEDULER_UNLOCK(intSave); } 解读 先是判断执行条件,确实是有信号需要处理,有处理函数.自定义处理函数是由用户进程安装进来的,所有进程旗下的任务都共用,参数就是信号signo,注意可不是系统调用号,有区别的.信号编号长这样. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 系统调用号长这样,是不是看到一些很熟悉的函数. #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 #define __NR_lchown 16 #define __NR_break 17 最后是最最最关键的代码,改变pc寄存器的值,此值一变,在_osExceptSwiHdl中恢复上下文后,cpu跳到用户空间的代码段 user.sighandle(R0,R1) 开始执行,即执行信号处理函数. sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 OsRestorSignalContext 恢复信号上下文 /**************************************************** 恢复信号上下文,由系统调用之__NR_sigreturn产生,这是一个内部产生的系统调用. 为什么要恢复呢? 因为系统调用的执行由任务内核态完成,使用的栈也是内核栈,CPU相关寄存器记录的都是内核栈的内容, 而系统调用完成后,需返回任务的用户栈执行,这时需将CPU各寄存器回到用户态现场 所以函数的功能就变成了还原寄存器的值 ****************************************************/ void OsRestorSignalContext(unsigned int *sp) { LosTaskCB *task = NULL; /* Do not adjust this statement */ LosProcessCB *process = NULL; sig_cb *sigcb = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); task = OsCurrTaskGet(); sigcb = &task->sig;//获取当前任务信号控制块 if (sigcb->context.count != 1) {//必须之前保存过,才能被恢复 SCHEDULER_UNLOCK(intSave); PRINT_ERR("sig error count : %d\n", sigcb->context.count); return; } process = OsCurrProcessGet();//获取当前进程 sp[REG_PC] = sigcb->context.PC;//指令寄存器 OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序状态寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 sp[REG_R0] = sigcb->context.R0; sp[REG_R1] = sigcb->context.R1; sp[REG_R2] = sigcb->context.R2; sp[REG_R3] = sigcb->context.R3; sp[REG_R7] = sigcb->context.R7; sp[REG_R12] = sigcb->context.R12; sigcb->context.count--; //信号上下文的数量回到减少 process->sigShare = 0; //回到用户态,信号共享清0 OsProcessExitCodeSignalClear(process);//清空进程退出码 SCHEDULER_UNLOCK(intSave); } 解读 在信号处理函数完成之后,内核会触发一个__NR_sigreturn的系统调用,又陷入内核态,回到了OsArmA32SyscallHandle. 恢复的过程很简单,把之前保存的信号上下文恢复到内核栈sp开始位置,数据在栈中的保存顺序可查看 用栈方式篇 ,最重要的看这几句. sp[REG_PC] = sigcb->context.PC;//指令寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USP和ULR指向的是用户栈的位置.一旦PC,USP,ULR从栈中弹出赋给寄存器.才真正完成了内核栈到用户栈的切换.回到了user.source()继续运行. 真正的切换汇编代码如下,都已添加注释,在保存和恢复上下文中夹着OsArmA32SyscallHandle @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理,注意此时已在内核栈运行 @保存任务上下文(TaskContext) 开始... 一定要对照TaskContext来理解 SUB SP, SP, #(4 * 16) @先申请16个栈空间单元用于处理本次软中断 STMIA SP, {R0-R12} @TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP SUB SP, SP, #4 @ => TaskContext.resved PUSH_FPU_REGS R1 @保存中断模式(用户模式) @保存任务上下文(TaskContext) 结束 MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/ CPSID I @执行后续指令前必须先关中断 @恢复任务上下文(TaskContext) 开始 POP_FPU_REGS R1 @弹出FPU值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 @恢复任务上下文(TaskContext) 结束 具体也可看这两篇: v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 进入 >> oschina | csdn | 51cto | 掘金 | harmony 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处.

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号消费篇) | 用户栈到内核栈的两次切换 | 百篇博客分析HarmonyOS源码 | v49.03

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号消费 本篇为信号消费篇,读之前建议先阅读信号生产篇,信号部分姊妹篇如下: v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > 本篇有相当的难度,涉及用户栈和内核栈的两次切换,寄存器改值,将围绕下图来说明. 解读 为本篇理解方便,把图做简化标签说明: user:用户空间 kernel:内核空间 source(...):源函数 sighandle(...):信号处理函数, syscall(...):系统调用,参数为系统调用号,如sigreturn,N(表任意) user.source():表示在用户空间运行的源函数 系列篇已多次说过,用户态的任务有两个运行栈,一个是用户栈,一个是内核栈.栈空间分别来自用户空间和内核空间.两种空间是有严格的地址划分的,通过虚拟地址的大小就能判断出是用户空间还是内核空间.系统调用本质上是软中断,它使CPU执行指令的场地由用户栈变成内核栈.怎么变的并不复杂,就是改变(sp和cpsr寄存器的值).sp指向哪个栈就代表在哪个栈运行, 当cpu在用户栈运行时是不能访问内核空间的,但内核态任务可以访问整个空间,而且内核态任务没有用户栈. 理解了上面的说明,再来说下正常系统调用流程是这样的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()继续运行,就必须保存用户栈现场各寄存器的值.这些值保存在内核栈中,恢复也是从内核栈恢复. 信号消费的过程的上图可简化表示为: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中间插入了信号处理函数的调用. 这正是本篇要通过代码来说清楚的核心问题. 顺着这个思路可以推到以下几点,实际也是这么做的: kernel.syscall(N) 中必须要再次保存user.source()的上下文sig_switch_context,为何已经保存了一次还要再保存一次? 因为第一次是保存在内核栈中,而内核栈这部分数据会因回到用户态user.sighandle()运行而被恢复现场出栈了.保存现场/恢复现场是成双出队的好基友,注意有些文章说会把整个内核栈清空,这是不对的. 第二次保存在任务结构体中,任务来源于任务池,是内核全局变量,常驻内存的.两次保存的都是user.source()运行时现场信息,再回顾下相关的结构体.关键是sig_switch_context typedef struct { // ... sig_cb sig;//信号控制块,用于异步通信 } LosTaskCB; typedef struct {//信号控制块(描述符) sigset_t sigFlag; //不屏蔽的信号集 sigset_t sigPendFlag; //信号阻塞标签集,记录那些信号来过,任务依然阻塞的集合.即:这些信号不能唤醒任务 sigset_t sigprocmask; /* Signals that are blocked */ //任务屏蔽了哪些信号 sq_queue_t sigactionq; //信号捕捉队列 LOS_DL_LIST waitList; //等待链表,上面挂的是等待信号到来的任务, 请查找 OsTaskWait(&sigcb->waitList, timeout, TRUE) 理解 sigset_t sigwaitmask; /* Waiting for pending signals */ //任务在等待哪些信号的到来 siginfo_t sigunbinfo; /* Signal info when task unblocked */ //任务解锁时的信号信息 sig_switch_context context; //信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换 } sig_cb; 还必须要改变原有PC/R0/R1寄存器的值.想要执行user.sighandle(),PC寄存器就必须指向它,而R0,R1就是它的参数. 信号处理完成后须回到内核态,怎么再次陷入内核态? 答案是:__NR_sigreturn,这也是个系统调用.回来后还原sig_switch_context,即还原user.source()被打断时SP/PC等寄存器的值,使其跳回到用户栈从user.source()的被打断处继续执行. 有了这三个推论,再理解下面的代码就是吹灰之力了,涉及三个关键函数 OsArmA32SyscallHandle,OsSaveSignalContext,OsRestorSignalContext本篇一一解读,彻底挖透.先看信号上下文结构体sig_switch_context. sig_switch_context //任务中断上下文 #define TASK_IRQ_CONTEXT \ unsigned int R0; \ unsigned int R1; \ unsigned int R2; \ unsigned int R3; \ unsigned int R12; \ unsigned int USP; \ unsigned int ULR; \ unsigned int CPSR; \ unsigned int PC; typedef struct {//信号切换上下文 TASK_IRQ_CONTEXT unsigned int R7; //存放系统调用的ID unsigned int count; //记录是否保存了信号上下文 } sig_switch_context; 保存user.source()现场的结构体,USP,ULR代表用户栈指针和返回地址. CPSR寄存器用于设置CPU的工作模式,CPU有7种工作模式,具体可前往翻看 v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > 谈论的用户态(usr普通用户)和内核态(sys超级用户)对应的只是其中的两种.二者都共用相同的寄存器.还原它就是告诉CPU内核已切到普通用户模式运行. 其他寄存器没有保存的原因是系统调用不会用到它们,所以不需要保存. R7是在系统调用发生时用于记录系统调用号,在信号处理过程中,R0将获得信号编号,作为user.sighandle()的第一个参数. count记录是否保存了信号上下文 OsArmA32SyscallHandle 系统调用总入口 /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */ /****************************************************************** 由汇编调用,见于 los_hw_exc.s / BLX OsArmA32SyscallHandle SYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号 regs:参数就是所有寄存器 注意:本函数在用户态和内核态下都可能被调用到 //MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 ******************************************************************/ LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用 if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break; case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//如果有信号要处理,将改写pc,r0,r1寄存器,改变返回正常用户态路径,而先去执行信号处理程序. /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with the SYS_context_switch system call. */ return regs;//返回寄存器的值 } 解读 这是系统调用的总入口,所有的系统调用都要跑这里要统一处理.通过系统号(保存在R7),找到注册函数并回调.完成系统调用过程. 关于系统调用可查看 v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > 本篇不详细说系统调用过程,只说跟信号相关的部分. OsArmA32SyscallHandle总体理解起来是被信号的保存和还原两个函数给包夹了.注意要在运行过程中去理解调用两个函数的过程,对于同一个任务来说,一定是先执行OsSaveSignalContext,第二次进入OsArmA32SyscallHandle后再执行OsRestorSignalContext. 看OsSaveSignalContext,由它负责保存user.source() 的上下文,其中改变了sp,r0/r1寄存器值,切到信号处理函数user.sighandle()运行. 在函数的开头,碰到系统调用号__NR_sigreturn,直接恢复信号上下文就退出了,因为这是要切回user.source()继续运行的操作. //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } OsSaveSignalContext 保存信号上下文 有了上面的铺垫,就不难理解这个函数的作用. /********************************************** 产生系统调用时,也就是软中断时,保存用户栈寄存器现场信息 改写PC寄存器的值 **********************************************/ void OsSaveSignalContext(unsigned int *sp) { UINTPTR sigHandler; UINT32 intSave; LosTaskCB *task = NULL; LosProcessCB *process = NULL; sig_cb *sigcb = NULL; unsigned long cpsr; OS_RETURN_IF_VOID(sp == NULL); cpsr = OS_SYSCALL_GET_CPSR(sp);//获取系统调用时的 CPSR值 OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必须工作在CPU的用户模式下,注意CPSR_USER_MODE(cpu层面)和OS_USER_MODE(系统层面)是两码事. SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信号分发/信号处理篇 task = OsCurrTaskGet(); process = OsCurrProcessGet(); sigcb = &task->sig;//获取任务的信号控制块 //1.未保存任务上下文任务 //2.任何的信号标签集不为空或者进程有信号要处理 if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) { sigHandler = OsGetSigHandler();//获取信号处理函数 if (sigHandler == 0) {//信号没有注册 sigcb->sigFlag = 0; process->sigShare = 0; SCHEDULER_UNLOCK(intSave); PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID); return; } /* One pthread do the share signal */ sigcb->sigFlag |= process->sigShare;//扩展任务的信号标签集 unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1; OsProcessExitCodeSignalSet(process, signo);//设置进程退出信号 sigcb->context.CPSR = cpsr; //保存状态寄存器 sigcb->context.PC = sp[REG_PC]; //获取被打断现场寄存器的值 sigcb->context.USP = sp[REG_SP];//用户栈顶位置,以便能从内核栈切回用户栈 sigcb->context.ULR = sp[REG_LR];//用户栈返回地址 sigcb->context.R0 = sp[REG_R0]; //系统调用的返回值 sigcb->context.R1 = sp[REG_R1]; sigcb->context.R2 = sp[REG_R2]; sigcb->context.R3 = sp[REG_R3]; sigcb->context.R7 = sp[REG_R7];//为何参数不用传R7,是因为系统调用发生时 R7始终保存的是系统调用号. sigcb->context.R12 = sp[REG_R12];//详见 https://my.oschina.net/weharmony/blog/4967613 sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 /* sig No bits 00000100 present sig No 3, but 1<< 3 = 00001000, so signo needs minus 1 */ sigcb->sigFlag ^= 1ULL << (signo - 1); sigcb->context.count++; //代表已保存 } SCHEDULER_UNLOCK(intSave); } 解读 先是判断执行条件,确实是有信号需要处理,有处理函数.自定义处理函数是由用户进程安装进来的,所有进程旗下的任务都共用,参数就是信号signo,注意可不是系统调用号,有区别的.信号编号长这样. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 系统调用号长这样,是不是看到一些很熟悉的函数. #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 #define __NR_lchown 16 #define __NR_break 17 最后是最最最关键的代码,改变pc寄存器的值,此值一变,在_osExceptSwiHdl中恢复上下文后,cpu跳到用户空间的代码段 user.sighandle(R0,R1) 开始执行,即执行信号处理函数. sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 OsRestorSignalContext 恢复信号上下文 /**************************************************** 恢复信号上下文,由系统调用之__NR_sigreturn产生,这是一个内部产生的系统调用. 为什么要恢复呢? 因为系统调用的执行由任务内核态完成,使用的栈也是内核栈,CPU相关寄存器记录的都是内核栈的内容, 而系统调用完成后,需返回任务的用户栈执行,这时需将CPU各寄存器回到用户态现场 所以函数的功能就变成了还原寄存器的值 ****************************************************/ void OsRestorSignalContext(unsigned int *sp) { LosTaskCB *task = NULL; /* Do not adjust this statement */ LosProcessCB *process = NULL; sig_cb *sigcb = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); task = OsCurrTaskGet(); sigcb = &task->sig;//获取当前任务信号控制块 if (sigcb->context.count != 1) {//必须之前保存过,才能被恢复 SCHEDULER_UNLOCK(intSave); PRINT_ERR("sig error count : %d\n", sigcb->context.count); return; } process = OsCurrProcessGet();//获取当前进程 sp[REG_PC] = sigcb->context.PC;//指令寄存器 OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序状态寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 sp[REG_R0] = sigcb->context.R0; sp[REG_R1] = sigcb->context.R1; sp[REG_R2] = sigcb->context.R2; sp[REG_R3] = sigcb->context.R3; sp[REG_R7] = sigcb->context.R7; sp[REG_R12] = sigcb->context.R12; sigcb->context.count--; //信号上下文的数量回到减少 process->sigShare = 0; //回到用户态,信号共享清0 OsProcessExitCodeSignalClear(process);//清空进程退出码 SCHEDULER_UNLOCK(intSave); } 解读 在信号处理函数完成之后,内核会触发一个__NR_sigreturn的系统调用,又陷入内核态,回到了OsArmA32SyscallHandle. 恢复的过程很简单,把之前保存的信号上下文恢复到内核栈sp开始位置,数据在栈中的保存顺序可查看 用栈方式篇 ,最重要的看这几句. sp[REG_PC] = sigcb->context.PC;//指令寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USP和ULR指向的是用户栈的位置.一旦PC,USP,ULR从栈中弹出赋给寄存器.才真正完成了内核栈到用户栈的切换.回到了user.source()继续运行. 真正的切换汇编代码如下,都已添加注释,在保存和恢复上下文中夹着OsArmA32SyscallHandle @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理,注意此时已在内核栈运行 @保存任务上下文(TaskContext) 开始... 一定要对照TaskContext来理解 SUB SP, SP, #(4 * 16) @先申请16个栈空间单元用于处理本次软中断 STMIA SP, {R0-R12} @TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP SUB SP, SP, #4 @ => TaskContext.resved PUSH_FPU_REGS R1 @保存中断模式(用户模式) @保存任务上下文(TaskContext) 结束 MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/ CPSID I @执行后续指令前必须先关中断 @恢复任务上下文(TaskContext) 开始 POP_FPU_REGS R1 @弹出FPU值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 @恢复任务上下文(TaskContext) 结束 具体也可看这两篇: v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 比自旋锁丰满许多的互斥锁 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 进入 >> oschina | csdn | 51cto | 掘金 | harmony 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处.

优秀的个人博客,低调大师

| 百篇博客分析HarmonyOS源码 | v47.01

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 进程关系链 进程是家族式管理的,父子关系,兄弟关系,朋友关系,子女关系,甚至陌生人关系(等待你消亡)在一个进程的生命周期中都会记录下来.用什么来记录呢?当然是内核最重要的胶水结构体LOS_DL_LIST,进程控制块(以下简称PCB)用了8个双向链表来记录进程家族的基因关系和运行时关系.如下: typedef struct ProcessCB { //...此处省略其他变量 LOS_DL_LIST pendList; /**< Block list to which the process belongs */ //进程所属的阻塞列表,如果因拿锁失败,就由此节点挂到等锁链表上 LOS_DL_LIST childrenList; /**< my children process list */ //孩子进程都挂到这里,形成双循环链表 LOS_DL_LIST exitChildList; /**< my exit children process list */ //那些要退出孩子进程挂到这里,白发人送黑发人。 LOS_DL_LIST siblingList; /**< linkage in my parent's children list */ //兄弟进程链表, 56个民族是一家,来自同一个父进程. LOS_DL_LIST subordinateGroupList; /**< linkage in my group list */ //进程是组长时,有哪些组员进程 LOS_DL_LIST threadSiblingList; /**< List of threads under this process *///进程的线程(任务)列表 LOS_DL_LIST threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules thepriority hash table */ //进程的线程组调度优先级哈希表 LOS_DL_LIST waitList; /**< The process holds the waitLits to support wait/waitpid *///进程持有等待链表以支持wait/waitpid } LosProcessCB; 解读 pendList 个人认为它是鸿蒙内核功能最多的一个链表,它远不止字面意思阻塞链表这么简单,只有深入解读源码后才能体会它真的是太会来事了,一般把它理解为阻塞链表就行.上面挂的是处于阻塞状态的进程. childrenList孩子链表,所有由它fork出来的进程都挂到这个链表上.上面的孩子进程在死亡前会将自己从上面摘出去,转而挂到exitChildList链表上. exitChildList退出孩子链表,进入死亡程序的进程要挂到这个链表上,一个进程的死亡是件挺麻烦的事,进程池的数量有限,需要及时回收进程资源,但家族管理关系复杂,要去很多地方消除痕迹.尤其还有其他进程在看你笑话,等你死亡(wait/waitpid)了通知它们一声. siblingList兄弟链表,和你同一个父亲的进程都挂到了这个链表上. subordinateGroupList 朋友圈链表,里面是因为兴趣爱好(进程组)而挂在一起的进程,它们可以不是一个父亲,不是一个祖父,但一定是同一个老祖宗(用户态和内核态根进程). threadSiblingList线程链表,上面挂的是进程ID都是这个进程的线程(任务),进程和线程的关系是1:N的关系,一个线程只能属于一个进程.这里要注意任务在其生命周期中是不能改所属进程的. threadPriQueueList线程的调度队列数组,一共32个,任务和进程一样有32个优先级,调度算法的过程是先找到优先级最高的进程,在从该进程的任务队列里去最高的优先级任务运行. waitList 是等待子进程消亡的任务链表,注意上面挂的是任务.任务是通过系统调用 pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); 将任务挂到waitList上.鸿蒙waitpid系统调用为SysWait,稍后会讲. 进程正常死亡过程 一个进程的自然消亡过程如下 //一个进程的自然消亡过程,参数是当前运行的任务 STATIC VOID OsProcessNaturalExit(LosTaskCB *runTask, UINT32 status) { LosProcessCB *processCB = OS_PCB_FROM_PID(runTask->processID);//通过task找到所属PCB LosProcessCB *parentCB = NULL; LOS_ASSERT(!(processCB->threadScheduleMap != 0));//断言没有任务需要调度了,当前task是最后一个了 LOS_ASSERT(processCB->processStatus & OS_PROCESS_STATUS_RUNNING);//断言必须为正在运行的进程 OsChildProcessResourcesFree(processCB);//释放孩子进程的资源 #ifdef LOSCFG_KERNEL_CPUP OsCpupClean(processCB->processID); #endif /* is a child process */ if (processCB->parentProcessID != OS_INVALID_VALUE) {//判断是否有父进程 parentCB = OS_PCB_FROM_PID(processCB->parentProcessID);//获取父进程实体 LOS_ListDelete(&processCB->siblingList);//将自己从兄弟链表中摘除,家人们,永别了! if (!OsProcessExitCodeSignalIsSet(processCB)) {//是否设置了退出码? OsProcessExitCodeSet(processCB, status);//将进程状态设为退出码 } LOS_ListTailInsert(&parentCB->exitChildList, &processCB->siblingList);//挂到父进程的孩子消亡链表,家人中,永别的可不止我一个. LOS_ListDelete(&processCB->subordinateGroupList);//和志同道合的朋友们永别了,注意家里可不一定是朋友的,所有各有链表. LOS_ListTailInsert(&processCB->group->exitProcessList, &processCB->subordinateGroupList);//挂到进程组消亡链表,朋友中,永别的可不止我一个. OsWaitCheckAndWakeParentProcess(parentCB, processCB);//检查父进程的等待任务并唤醒任务,此处将会切换到其他任务运行. OsDealAliveChildProcess(processCB);//老父亲临终向各自的祖宗托孤 processCB->processStatus |= OS_PROCESS_STATUS_ZOMBIES;//贴上僵死进程的标签 (VOID)OsKill(processCB->parentProcessID, SIGCHLD, OS_KERNEL_KILL_PERMISSION);//以内核权限发送SIGCHLD(子进程退出)信号. LOS_ListHeadInsert(&g_processRecyleList, &processCB->pendList);//将进程通过其阻塞节点挂入全局进程回收链表 OsRunTaskToDelete(runTask);//删除正在运行的任务 return; } LOS_Panic("pid : %u is the root process exit!\n", processCB->processID); return; } 解读 退群,向兄弟姐妹siblingList告别,向朋友圈(进程组)告别subordinateGroupList. 留下你的死亡记录,老父亲记录到exitChildList,朋友圈记录到exitProcessList中. 告诉后人死亡原因OsProcessExitCodeSet,因为waitList上挂的任务在等待你的死亡信息. 向老祖宗托孤,用户态和内核态进程都有自己的祖宗进程(1和2号进程),老祖宗身子硬朗,最后死.所有的短命鬼进程都可以把自己的孩子委托给老祖宗照顾,老祖宗会一视同仁. 将自己变成了OS_PROCESS_STATUS_ZOMBIES僵尸进程. 老父亲跑到村口广播这个孩子已经死亡的信号OsKill. 将自己挂入进程回收链表,等待回收任务ResourcesTask回收资源. 最后删除这个正在运行的任务,很明显其中一定会发生一次调度OsSchedResched. //删除一个正在运行的任务 LITE_OS_SEC_TEXT VOID OsRunTaskToDelete(LosTaskCB *taskCB) { LosProcessCB *processCB = OS_PCB_FROM_PID(taskCB->processID);//拿到task所属进程 OsTaskReleaseHoldLock(processCB, taskCB);//task还锁 OsTaskStatusUnusedSet(taskCB);//task重置为未使用状态,等待回收 LOS_ListDelete(&taskCB->threadList);//从进程的线程链表中将自己摘除 processCB->threadNumber--;//进程的活动task --,注意进程还有一个记录总task的变量 processCB->threadCount LOS_ListTailInsert(&g_taskRecyleList, &taskCB->pendList);//将task插入回收链表,等待回收资源再利用 OsEventWriteUnsafe(&g_resourceEvent, OS_RESOURCE_EVENT_FREE, FALSE, NULL);//发送释放资源的事件,事件由 OsResourceRecoveryTask 消费 OsSchedResched();//申请调度 return; } 但这是一个自然死亡的进程,还有很多非正常死亡在其他篇幅中已有说明.请自行翻看.非正常死亡的会产生僵尸进程.这种进程需要别的进程通过 waitpid来回收. 孤儿进程 一般情况下往往是白发人送黑发人,子进程的生命周期是要短于父进程.但因为fork之后,进程之间相互独立,调度算法一视同仁,父子之间是弱的关系力,就什么情况都可能发生了.内核是允许老父亲先走的,如果父进程退出而它的一个或多个子进程还在运行,那么这些子进程就被称为孤儿进程,孤儿进程最终将被两位老祖宗(用户态和内核态)所收养,并由老祖宗完成对它们的状态收集工作。 //当一个进程自然退出的时候,它的孩子进程由两位老祖宗收养 STATIC VOID OsDealAliveChildProcess(LosProcessCB *processCB) { UINT32 parentID; LosProcessCB *childCB = NULL; LosProcessCB *parentCB = NULL; LOS_DL_LIST *nextList = NULL; LOS_DL_LIST *childHead = NULL; if (!LOS_ListEmpty(&processCB->childrenList)) {//如果存在孩子进程 childHead = processCB->childrenList.pstNext;//获取孩子链表 LOS_ListDelete(&(processCB->childrenList));//清空自己的孩子链表 if (OsProcessIsUserMode(processCB)) {//是用户态进程 parentID = g_userInitProcess;//用户态进程老祖宗 } else { parentID = g_kernelInitProcess;//内核态进程老祖宗 } for (nextList = childHead; ;) {//遍历孩子链表 childCB = OS_PCB_FROM_SIBLIST(nextList);//找到孩子的真身 childCB->parentProcessID = parentID;//孩子磕头认老祖宗为爸爸 nextList = nextList->pstNext;//找下一个孩子进程 if (nextList == childHead) {//一圈下来,孩子们都磕完头了 break; } } parentCB = OS_PCB_FROM_PID(parentID);//找个老祖宗的真身 LOS_ListTailInsertList(&parentCB->childrenList, childHead);//挂到老祖宗的孩子链表上 } return; } 解读 函数很简单,都一一注释了,老父亲临终托付后事,请各自的老祖宗照顾孩子. 从这里也可以看出进程的家族管理模式,两个家族从进程的出生到死亡负责到底. 僵尸进程 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。 如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程,即 Z 进程.任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了. 不正常情况下就需要手动waitpid清理了. waitpid 在鸿蒙系统中,一个进程结束了,但是它的父进程没有等待(调用wait waitpid)它,那么它将变成一个僵尸进程。通过系统调用 waitpid可以彻底的清理掉子进程.归还pcb.最终调用到SysWait #include <sys/wait.h> #include "syscall.h" pid_t waitpid(pid_t pid, int *status, int options) { return syscall_cp(SYS_wait4, pid, status, options, 0); } //等待子进程结束 int SysWait(int pid, USER int *status, int options, void *rusage) { (void)rusage; return LOS_Wait(pid, status, (unsigned int)options, NULL); } //返回已经终止的子进程的进程ID号,并清除僵死进程。 LITE_OS_SEC_TEXT INT32 LOS_Wait(INT32 pid, USER INT32 *status, UINT32 options, VOID *rusage) { (VOID)rusage; UINT32 ret; UINT32 intSave; LosProcessCB *childCB = NULL; LosProcessCB *processCB = NULL; LosTaskCB *runTask = NULL; ret = OsWaitOptionsCheck(options);//参数检查,只支持LOS_WAIT_WNOHANG if (ret != LOS_OK) { return -ret; } SCHEDULER_LOCK(intSave); processCB = OsCurrProcessGet(); //获取当前进程 runTask = OsCurrTaskGet(); //获取当前任务 ret = OsWaitChildProcessCheck(processCB, pid, &childCB);//先检查下看能不能找到参数要求的退出子进程 if (ret != LOS_OK) { pid = -ret; goto ERROR; } if (childCB != NULL) {//找到了进程 return OsWaitRecycleChildPorcess(childCB, intSave, status);//回收进程 } //没有找到,看是否要返回还是去做个登记 if ((options & LOS_WAIT_WNOHANG) != 0) {//有LOS_WAIT_WNOHANG标签 runTask->waitFlag = 0;//等待标识置0 pid = 0;//这里置0,是为了 return 0 goto ERROR; } //等待孩子进程退出 OsWaitInsertWaitListInOrder(runTask, processCB);//将当前任务挂入进程waitList链表 //发起调度的目的是为了让出CPU,让其他进程/任务运行 OsSchedResched();//发起调度 runTask->waitFlag = 0; if (runTask->waitID == OS_INVALID_VALUE) { pid = -LOS_ECHILD;//没有此子进程 goto ERROR; } childCB = OS_PCB_FROM_PID(runTask->waitID);//获取当前任务的等待子进程ID if (!(childCB->processStatus & OS_PROCESS_STATUS_ZOMBIES)) {//子进程非僵死进程 pid = -LOS_ESRCH;//没有此进程 goto ERROR; } //回收僵死进程 return OsWaitRecycleChildPorcess(childCB, intSave, status); ERROR: SCHEDULER_UNLOCK(intSave); return pid; } 解读 pid是数据参数,根据不同的参数代表不同的含义,含义如下: 参数值 说明 pid<-1 等待进程组号为pid绝对值的任何子进程。 pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。 pid>0 等待进程号为pid的子进程。 pid不同值代表的真正含义可以看这个函数OsWaitSetFlag. //设置等待子进程退出方式方法 STATIC UINT32 OsWaitSetFlag(const LosProcessCB *processCB, INT32 pid, LosProcessCB **child) { LosProcessCB *childCB = NULL; ProcessGroup *group = NULL; LosTaskCB *runTask = OsCurrTaskGet(); UINT32 ret; if (pid > 0) {//等待进程号为pid的子进程结束 /* Wait for the child process whose process number is pid. */ childCB = OsFindExitChildProcess(processCB, pid);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID,注意一个进程退出时会挂到父进程的exitChildList上 goto WAIT_BACK;//直接成功返回 } ret = OsFindChildProcess(processCB, pid);//看能否从现有的孩子链表中找到PID if (ret != LOS_OK) { return LOS_ECHILD;//参数进程并没有这个PID孩子,返回孩子进程失败. } runTask->waitFlag = OS_PROCESS_WAIT_PRO;//设置当前任务的等待类型 runTask->waitID = pid; //当前任务要等待进程ID结束 } else if (pid == 0) {//等待同一进程组中的任何子进程 /* Wait for any child process in the same process group */ childCB = OsFindGroupExitProcess(processCB->group, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID goto WAIT_BACK;//直接成功返回 } runTask->waitID = processCB->group->groupID;//等待进程组的任意一个子进程结束 runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型 } else if (pid == -1) {//等待任意子进程 /* Wait for any child process */ childCB = OsFindExitChildProcess(processCB, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID goto WAIT_BACK; } runTask->waitID = pid;//等待PID,这个PID可以和当前进程没有任何关系 runTask->waitFlag = OS_PROCESS_WAIT_ANY;//设置当前任务的等待类型 } else { /* pid < -1 */ //等待指定进程组内为|pid|的所有子进程 /* Wait for any child process whose group number is the pid absolute value. */ group = OsFindProcessGroup(-pid);//先通过PID找到进程组 if (group == NULL) { return LOS_ECHILD; } childCB = OsFindGroupExitProcess(group, OS_INVALID_VALUE);//在进程组里任意一个已经退出的子进程 if (childCB != NULL) { goto WAIT_BACK; } runTask->waitID = -pid;//此处用负数是为了和(pid == 0)以示区别,因为二者的waitFlag都一样. runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型 } WAIT_BACK: *child = childCB; return LOS_OK; } status带走进程退出码,exitCode分成了三个部分格式如下 /* * Process exit code * 31 15 8 7 0 * | | exit code | core dump | signal | */ #define OS_PRO_EXIT_OK 0 //进程正常退出 //置进程退出码第七位为1 STATIC INLINE VOID OsProcessExitCodeCoreDumpSet(LosProcessCB *processCB) { processCB->exitCode |= 0x80U;// 0b10000000 } //设置进程退出信号(0 ~ 7) STATIC INLINE VOID OsProcessExitCodeSignalSet(LosProcessCB *processCB, UINT32 signal) { processCB->exitCode |= signal & 0x7FU;//0b01111111 } //清除进程退出信号(0 ~ 7) STATIC INLINE VOID OsProcessExitCodeSignalClear(LosProcessCB *processCB) { processCB->exitCode &= (~0x7FU);//低7位全部清0 } //进程退出码是否被设置过,默认是 0 ,如果 & 0x7FU 还是 0 ,说明没有被设置过. STATIC INLINE BOOL OsProcessExitCodeSignalIsSet(LosProcessCB *processCB) { return (processCB->exitCode) & 0x7FU; } //设置进程退出号(8 ~ 15) STATIC INLINE VOID OsProcessExitCodeSet(LosProcessCB *processCB, UINT32 code) { processCB->exitCode |= ((code & 0x000000FFU) << 8U) & 0x0000FF00U; /* 8: Move 8 bits to the left, exitCode */ } 0 - 7为信号位,信号处理有专门的篇幅,此处不做详细介绍,请自行翻看,这里仅列出部分信号含义. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(如break键被按下) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //终止信号 #define SIGSTKFLT 16 //栈溢出 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 | 不能被忽略、处理和阻塞 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 #define SIGTTIN 21 //进程停止,后台进程企图从控制终端读 #define SIGTTOU 22 //进程停止,后台进程企图从控制终端写 #define SIGURG 23 //I/O有紧急数据到达当前进程 #define SIGXCPU 24 //进程的CPU时间片到期 #define SIGXFSZ 25 //文件大小的超出上限 #define SIGVTALRM 26 //虚拟时钟超时 #define SIGPROF 27 //profile时钟超时 #define SIGWINCH 28 //窗口大小改变 #define SIGIO 29 //I/O相关 #define SIGPOLL 29 // #define SIGPWR 30 //电源故障,关机 #define SIGSYS 31 //系统调用中参数错,如系统调用号非法 #define SIGUNUSED SIGSYS //系统调用异常 options是行为参数,提供了一些另外的选项来控制waitpid()函数的行为。 参数值 鸿蒙支持 说明 LOS_WAIT_WNOHANG 支持 如果没有孩子进程退出,则立即返回,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。 LOS_WAIT_WUNTRACED 不支持 报告终止或停止的子进程的状态 LOS_WAIT_WCONTINUED 不支持 鸿蒙目前只支持了LOS_WAIT_WNOHANG模式,内核源码中虽有LOS_WAIT_WUNTRACED和LOS_WAIT_WCONTINUED的实现痕迹,但是整体阅读下来比较乱,应该是没有写好. 鸿蒙源码百篇博客 往期回顾 v47.xx (进程回收篇) | 老父亲如何向老祖宗临终托孤 ? < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | 51cto | harmony > v36.xx (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (CPU篇) | 内核是如何描述CPU的? < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | CPU在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾CPU? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 内核500问你能答对多少? < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 就绪队列对调度的作用 < csdn | 51cto | harmony > v05.xx (任务管理篇) | 谁在让CPU忙忙碌碌? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 触发调度最大的动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 关注「鸿蒙内核源码分析」公众号 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处. 进入 >> oschina | csdn | 51cto | 简书 | 掘金 | harmony

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册