diff --git a/articles/20230919-elf2flt-elf2flt-src-analysis.md b/articles/20230919-elf2flt-elf2flt-src-analysis.md new file mode 100644 index 0000000000000000000000000000000000000000..14f4870c2bea4ab5bcb71408c443cf57a859894c --- /dev/null +++ b/articles/20230919-elf2flt-elf2flt-src-analysis.md @@ -0,0 +1,361 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [toc comments codeinline refs pangu]
+> Author: Odysseus <320873791@qq.com>
+> Date: 2023/09/19
+> Revisor: walimis <>
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [为 ELF2FLT 完善独立编译与安装支持](https://gitee.com/tinylab/riscv-linux/issues/I79PO2)
+> Sponsor: PLCT Lab, ISCAS + +# 从源码看 elf2flt 原理 + +## 前言 + +`elf2flt` 是一个专门设计用于将 ELF 二进制格式转换为 bFLT 二进制格式的工具,目的是使得二进制文件能够在没有内存管理单元 (MMU) 的 CPU 上正常运行。虽然 elf2flt 的代码总量达到了约两千行,但是其中的大部分代码是用于支持不同的 CPU 架构的。在本文中,我们将专门针对 RISC-V 架构进行深入探讨。 + +## 目录结构 + +elf2flt 的目录结构如下所示: + +```bash +. +├── elf2flt +│ ├── compress.c +│ ├── compress.h +│ ├── config.guess +│ ├── config.sub +│ ├── configure +│ ├── configure.ac +│ ├── e1-elf2flt.ld +│ ├── elf +│ │ ├── reloc-macros.h +│ │ └── riscv.h +│ ├── elf2flt.c +│ ├── elf2flt.ld.in +│ ├── filenames.h +│ ├── flat.h +│ ├── flthdr.c +│ ├── install-sh +│ ├── ld-elf2flt.c +│ ├── ld-elf2flt.in +│ ├── LICENSE.TXT +│ ├── Makefile.in +│ ├── README.md +│ ├── README_riscv64.md +│ ├── stubs.c +│ ├── stubs.h +│ ├── tests +│ │ ├── flthdr +│ │ │ ├── basic.good +│ │ │ ├── generate.c +│ │ │ ├── multi.good +│ │ │ └── test.sh +│ │ └── lib.sh +│ └── travis +│ ├── arches.sh +│ ├── lib.sh +│ └── main.sh +``` + +从目录结构我们可以看出,程序的主要入口是 `elf2flt.c`,这是一个大约有两千行 C 代码的文件。 `tests` 和 `travis` 目录都是作测试用途。 + +## 文件解析 + +程序首先对 ELF 文件进行解析,其主要的解析过程如下: + +1. 定义了一系列的变量用于存储输入文件和其他相关的信息。 +2. 初始化这些变量并准备进行文件解析。 +3. 使用 `bfd` 库来读取和解析输入文件的符号表和段信息。 +4. 根据段的属性(如代码段、数据段、`.bss` 段等)来确定段的位置和大小。 +5. 根据解析得到的信息,将这些段的内容读取到相应的内存区域中。 + +首先,我们来看 `main` 函数。在函数前段的定义中,包含了输入输出文件、操作流、操作、栈、符号表、各个数据段的大小和起止、数据段地址、重定位地址等变量。 + +```c + int fd; + bfd *rel_bfd, *abs_bfd; + asection *s; + char *ofile=NULL, *pfile=NULL, *abs_file = NULL, *rel_file = NULL; + char *fname = NULL; + int opt; + int i; + int stack; + stream gf; + + asymbol **symbol_table; + long number_of_symbols; + + uint32_t data_len = 0; + uint32_t bss_len = 0; + uint32_t text_len = 0; + uint32_t reloc_len; + + uint32_t data_vma = ~0; + uint32_t bss_vma = ~0; + uint32_t text_vma = ~0; + + uint32_t text_offs; + + void *text; + void *data; + uint32_t *reloc; +``` + +在对栈空间进行分配后,进行常规的参数处理,通过 `bfd` 库对输入文件格式进行错误检查。之后,我们开始正式进行转换。 + +转换的第一步,是解析输入文件,获取符号表和段表信息。 + +```C + symbol_table = get_symbols(abs_bfd, &number_of_symbols); + + /* Group output sections into text, data, and bss, and calc their sizes. */ + for (s = abs_bfd->sections; s != NULL; s = s->next) { + uint32_t *vma, *len; + bfd_size_type sec_size; + bfd_vma sec_vma; + + if ((s->flags & SEC_CODE) || + ro_reloc_data_section_should_be_in_text(s)) { + vma = &text_vma; + len = &text_len; + } else if (s->flags & SEC_DATA) { + vma = &data_vma; + len = &data_len; + } else if (s->flags & SEC_ALLOC) { + vma = &bss_vma; + len = &bss_len; + } else + continue; + + sec_size = elf2flt_bfd_section_size(s); + sec_vma = elf2flt_bfd_section_vma(s); + + if (sec_vma < *vma) { + if (*len > 0) + *len += sec_vma - *vma; + else + *len = sec_size; + *vma = sec_vma; + } else if (sec_vma + sec_size > *vma + *len) + *len = sec_vma + sec_size - *vma; + } +``` + +这里的 `get_symbols` 函数主要使用 `bfd` 库进行符号表的解析和填充。 + +而在 `sections` 的解析中,通过对节头元数据的判断来确定节的类型,并将其填入预先分配的地址中并获取其长度。 + +接着处理 `.text` 段,如果段长度为 0 则出错;如果不为 0,分配对应长度的内存分配,然后将特定类型的输入节(代码节和只读重定位数据节)读取到 `.text` 段里面: + +```C + for (s = abs_bfd->sections; s != NULL; s = s->next) + if ((s->flags & SEC_CODE) || + ro_reloc_data_section_should_be_in_text(s)) + if (!bfd_get_section_contents(abs_bfd, s, + text + (s->vma - text_vma), 0, + elf2flt_bfd_section_size(s))) + { + fatal("read error section %s", s->name); + } +``` + +接着,将其他的数据节(除了上述提到的只读重定位数据节)都读到数据输出段内,将符号表放入 `.bss` 段内,再进行一些必要的错误判断,初始的解析就完成了。 + +## 重定位处理 + +对于 RISC-V 架构,重定位的处理是关键的一步。简化的步骤如下: + +1. 判断哪些段需要进行重定位。 +2. 为需要重定位的段确定重定位的起始位置。 +3. 使用 `bfd` 库来解析输入文件的重定位条目。 +4. 根据这些条目和当前段的信息,更新段的内容以完成重定位。 + +我们具体看 elf2flt。首先,从源码中相应的调用我们可以看见: + +```C + reloc = (uint32_t *) + output_relocs(abs_bfd, symbol_table, number_of_symbols, &reloc_len, + text, text_len, text_vma, data, data_len, data_vma, rel_bfd); +``` + +重定位是通过调用 `output_relocs` 函数来完成的,这个函数占据了两千行中的一千行,是程序的核心代码。但实际上,大部分代码用来处理不同架构, RISC-V 相关的代码只占一小部分。 + +这里我们只以 RISC-V 为例,来看整个过程是怎么实现的。 + +这里首先获取 GOT 表的大小。由于 GOT 表终止于 -1,所以这一步不算困难。(这里的重定位和绝对地址都有这个终止记号) + +```C + if (pic_with_got && !use_resolved) { + uint32_t *lp = (uint32_t *)data; + /* Should call ntohl(*lp) here but is isn't going to matter */ + while (*lp != 0xffffffff) lp++; + got_size = ((unsigned char *)lp) - data; +``` + +接着,扫描每一个节,如果使用位置无关代码(PIC)或者是全局偏移表(GOT),则只针对可写数据节中进行重定位,否则也对 `.text` 段和只读数据进行重定位。 + +```C +if ((!pic_with_got || ALWAYS_RELOC_TEXT) && + ((a->flags & SEC_CODE) || + ro_reloc_data_section_should_be_in_text(a))) + sectionp = text + (a->vma - text_vma); +else if (a->flags & SEC_DATA) + sectionp = data + (a->vma - data_vma); +else + continue; +``` + +然后查找与当前处理的程序部分相关的二进制重定位文件中的信息,然后使用这些信息来构建重定位条目。 + +在进行一些检查排除了不需要进行重定位的情况后,正式开始进行重定位,这里针对不同的系统架构进行了不同的重定位解析,以 RISC-V 为例,如下: + +```C +#elif defined(TARGET_riscv64) + case R_RISCV_32_PCREL: + case R_RISCV_ADD32: + case R_RISCV_ADD64: + case R_RISCV_SUB32: + case R_RISCV_SUB64: + continue; + case R_RISCV_32: + case R_RISCV_64: + goto good_32bit_resolved_reloc; + default: + goto bad_resolved_reloc; +``` + +我们发现,只有 R_RISCV_32、R_RISCV_64 这两种重定位方式会进入到 `good_32bit_resolved_reloc` 这个标签中,这里的代码如下: + +```C +good_32bit_resolved_reloc: + if (bfd_big_endian (abs_bfd)) + sym_addr = + (r_mem[0] << 24) + + (r_mem[1] << 16) + + (r_mem[2] << 8) + + r_mem[3]; + else + sym_addr = + r_mem[0] + + (r_mem[1] << 8) + + (r_mem[2] << 16) + + (r_mem[3] << 24); + relocation_needed = 1; + update_text = 0; + break; +``` + +这里,我们可以看到,对于 32 位的重定位,我们将通过是否是大小端的判断,将其转换为一个 32 位的地址。然后,我们将其放入符号表中: + +```C +if (relocation_needed) { + if (verbose) + printf(" RELOC[%d]: offset=0x%"BFD_VMA_FMT"x symbol=%s%s " + "section=%s size=%d " + "fixup=0x%x (reloc=0x%"BFD_VMA_FMT"x)\n", + flat_reloc_count, + q->address, sym_name, addstr, + section_name, sym_reloc_size, + sym_addr, section_vma + q->address); + +#ifndef TARGET_bfin + flat_relocs = realloc(flat_relocs, + (flat_reloc_count + 1) * sizeof(uint32_t)); +#ifndef TARGET_e1 + flat_relocs[flat_reloc_count] = pflags | + (section_vma + q->address); + +#else + // ... +#endif + flat_reloc_count++; +#endif //TARGET_bfin + relocation_needed = 0; + pflags = 0; + } +``` + +这里可以看到,重定位地址由段地址加上相对偏移,再与 `pflags` 进行按位或操作完成。RISC-V 没有用到 `pflags` 。 + +到此处,我们实际上已经知道各段大小。我们的 GOT 表放在 `.data` 段的最上面,因此由此可以确定重定位入口,到此重定位完成。 + +## header 构建 + +为了能够成功地加载和执行 bFLT 格式的二进制文件,我们需要构建一个 bFLT header,这个 header 包含了关于二进制文件的所有必要信息。 + +```C + memcpy(hdr.magic,"bFLT",4); + hdr.rev = htonl(FLAT_VERSION); + hdr.entry = htonl(sizeof(hdr) + bfd_get_start_address(abs_bfd)); + hdr.data_start = htonl(sizeof(hdr) + text_offs + text_len); + hdr.data_end = htonl(sizeof(hdr) + text_offs + text_len +data_len); + hdr.bss_end = htonl(sizeof(hdr) + text_offs + text_len +data_len+bss_len); + hdr.stack_size = htonl(stack); /* FIXME */ + hdr.reloc_start = htonl(sizeof(hdr) + text_offs + text_len +data_len); + hdr.reloc_count = htonl(reloc_len); + hdr.flags = htonl(0 + | (load_to_ram || text_has_relocs ? FLAT_FLAG_RAM : 0) + | (ktrace ? FLAT_FLAG_KTRACE : 0) + | (pic_with_got ? FLAT_FLAG_GOTPIC : 0) + | (docompress ? (docompress == 2 ? FLAT_FLAG_GZDATA : FLAT_FLAG_GZIP) : 0) + ); + hdr.build_date = htonl((uint32_t)get_build_date()); + memset(hdr.filler, 0x00, sizeof(hdr.filler)); + + for (i=0; i