objcopy 如何计算要插入输出文件的 ELF 文件的哪些部分?

3

假设我运行了arm-none-eabi-objcopy firmwared.elf -O ihex firmware.hex

假设二进制文件是使用以下链接器脚本生成的:

ENTRY(Reset_Handler)

MEMORY
{
  FLASH (RX) : ORIGIN = 0x08020000, LENGTH = 896K
  SRAM (RWX) : ORIGIN = 0x20000000, LENGTH = 512K
  BKPSRAM (RW) : ORIGIN = 0x40024000, LENGTH = 4K
}

_estack = 0x20080000;

SECTIONS
{
  .isr_vector :
  {
    . = ALIGN(4);
    _isr_vector = .;
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } > FLASH

  .firmware_header_vector :
  {
    . = ALIGN(4);
    KEEP(*(.firmware_header_vector))
    . = ALIGN(4);
  } > FLASH

  .text :
  {
    . = ALIGN(4);
    _stext = .;
    *(.Reset_Handler)
    *(.text)
    *(.text*)
    *(.rodata)
    *(.rodata*)
    *(.glue_7)
    *(.glue_7t)
    KEEP(*(.init))
    KEEP(*(.fini))
    . = ALIGN(4);
    _etext = .;

  } > FLASH

  .ARM.extab :
  {
    . = ALIGN(4);
    *(.ARM.extab)
    *(.gnu.linkonce.armextab.*)
    . = ALIGN(4);
  } > FLASH

  .exidx :
  {
    . = ALIGN(4);
    PROVIDE(__exidx_start = .);
    *(.ARM.exidx*)
    . = ALIGN(4);
    PROVIDE(__exidx_end = .);
  } > FLASH

  .preinit_array :
  {
    PROVIDE(__preinit_array_start = .);
    KEEP(*(.preinit_array*))
    PROVIDE(__preinit_array_end = .);
  } > FLASH

  .init_array :
  {
    PROVIDE(__init_array_start = .);
    KEEP(*(SORT(.init_array.*)))
    KEEP(*(.init_array*))
    PROVIDE(__init_array_end = .);
  } > FLASH

  .fini_array :
  {
    PROVIDE(__fini_array_start = .);
    KEEP(*(.fini_array*))
    KEEP(*(SORT(.fini_array.*)))
    PROVIDE(__fini_array_end = .);
  } > FLASH

  _sidata = .;
  .data_x : AT(_sidata) /* LMA address is _sidata (in FLASH) */
  {
    . = ALIGN(4);
    _sdata = .; /* data section VMA address */
    *(.data*)
    . = ALIGN(4);
    _edata = .;
  } > SRAM

  .firmware_header (_sidata + SIZEOF(.data_x)):
  {
    . = ALIGN(4);
    KEEP(*(.firmware_header))
    . = ALIGN(4);
  } > FLASH

  .eth (NOLOAD) :
  {
    . = ALIGN(4);
    KEEP(*(.RxDecripSection))
    KEEP(*(.TxDescripSection))
    KEEP(*(.RxarraySection))
    KEEP(*(.TxarraySection))
    . = ALIGN(4);
  } > SRAM

  .bss :
  {
    . = ALIGN(4);
    _sbss = .;

    PROVIDE(__bss_start__ = _sbss);
    *(.bss)
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;

    PROVIDE(__bss_end__ = _ebss);
  } > SRAM

  PROVIDE(end = .);

  .heap (NOLOAD) :
  {
    . = ALIGN(4);
    PROVIDE(__heap_start__ = .);
    KEEP(*(.heap))
    . = ALIGN(4);
    PROVIDE(__heap_end__ = .);
  } > SRAM

  .reserved_for_stack (NOLOAD) :
  {
    . = ALIGN(4);
    PROVIDE(__reserved_for_stack_start__ = .);
    KEEP(*(.reserved_for_stack))
    . = ALIGN(4);
    PROVIDE(__reserved_for_stack_end__ = .);
  } > SRAM

  .battery_backed_sram (NOLOAD) :
  {
    . = ALIGN(4);
    KEEP(*(.battery_backed_sram))
    . = ALIGN(4);
  } > BKPSRAM

  /DISCARD/ :
  {
    *(.ARM.attributes)
  }
}

这将产生以下readelf输出:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .isr_vector       PROGBITS        08020000 010000 0001f8 00  WA  0   0  4
  [ 2] .firmware_header_ PROGBITS        080201f8 0101f8 000004 00  WA  0   0  4
  [ 3] .text             PROGBITS        08020200 010200 021b44 00  AX  0   0 64
  [ 4] .ARM.extab        PROGBITS        08041d44 042728 000000 00   W  0   0  1
  [ 5] .exidx            ARM_EXIDX       08041d44 031d44 000008 00  AL  3   0  4
  [ 6] .init_array       INIT_ARRAY      08041d4c 031d4c 000008 04  WA  0   0  4
  [ 7] .fini_array       FINI_ARRAY      08041d54 031d54 000004 04  WA  0   0  4
  [ 8] .data_x           PROGBITS        20000000 040000 0009c8 00  WA  0   0  8
  [ 9] .firmware_header  PROGBITS        08042720 042720 000008 00  WA  0   0  4
  [10] .eth              NOBITS          200009c8 0509c8 0030a0 00  WA  0   0  4
  [11] .bss              NOBITS          20003a68 0509c8 045da4 00  WA  0   0  4
  [12] .heap             PROGBITS        2004980c 042728 000000 00   W  0   0  1
  [13] .reserved_for_sta PROGBITS        2004980c 042728 000000 00   W  0   0  1
  [14] .battery_backed_s NOBITS          40024000 044000 00000c 00  WA  0   0  4
  [15] .comment          PROGBITS        00000000 042728 000075 01  MS  0   0  1
  [16] .debug_frame      PROGBITS        00000000 0427a0 00144c 00      0   0  4
  [17] .stab             PROGBITS        00000000 043bec 000084 0c     18   0  4
  [18] .stabstr          STRTAB          00000000 043c70 000117 00      0   0  1
  [19] .symtab           SYMTAB          00000000 043d88 009b00 10     20 1787  4
  [20] .strtab           STRTAB          00000000 04d888 0042bb 00      0   0  1
  [21] .shstrtab         STRTAB          00000000 051b43 0000e6 00      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),
  y (purecode), p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x010000 0x08020000 0x08020000 0x21d58 0x21d58 RWE 0x10000
  LOAD           0x040000 0x20000000 0x08041d58 0x009c8 0x009c8 RW  0x10000
  LOAD           0x042720 0x08042720 0x08042720 0x00008 0x00008 RW  0x10000
  LOAD           0x0509c8 0x200009c8 0x08042720 0x00000 0x48e44 RW  0x10000
  LOAD           0x044000 0x40024000 0x40024000 0x00000 0x0000c RW  0x10000

 Section to Segment mapping:
  Segment Sections...
   00     .isr_vector .firmware_header_vector .text .exidx .init_array .fini_array 
   01     .data_x 
   02     .firmware_header 
   03     .eth .bss 
   04     .battery_backed_sram 

objcopy如何知道不将.bss等节插入输出映像?我知道它是实时计算的,我假设这个机制是通过节到段的映射驱动的,但我找不到任何解释它如何“实际”执行段到节的映射的说明。Elf文件未存储有关哪些段是Flash的信息,但objcopy以某种方式知道它不应该将.bss复制到输出文件中。如何做到的?

由于它生成的是二进制图像,因此它主要从程序头部开始。那些不被加载到任何段中的部分最终将被忽略。固定为零(NOBITS)的部分也可能不会出现在二进制文件中。 - Chris Dodd
这很有道理,但这是否意味着它必须遍历两个部分和程序头,然后“连接”该信息以说明将加载哪个?因为根据程序头,没有链接到特定的部分被给定的程序头所覆盖。据我所知,获取该信息的唯一方法是使用(文件偏移量+FileSiz),然后查找在该范围内发生的块,以了解哪些部分在该程序头中。这正确吗? - chris12892
此外,我感到困惑的原因是.heap被标记为PROGBITS,但它没有被加载。我想我真正想要的是一个简单的算法,可以用来决定一个部分是否被加载,最好是与objcopy执行相同的方式。 - chris12892
哦,实际上我刚注意到堆的大小为0。那可能就是问题所在了。 - chris12892
这些段包含文件偏移量和大小,这应该足以知道要复制什么,而不必查看部分内容,是吧? - PaulR
显示剩余3条评论
4个回答

1

readelf 输出中 Flg 列中的 'A' 标志,ELF 称之为 SHF_ALLOC,表示一个在进程执行期间占用内存的部分。

SHF_ALLOC:该部分在进程执行期间占用内存。一些控制部分不驻留在对象文件的内存映像中;对于这些部分,此属性关闭。

http://refspecs.linuxbase.org/elf/elf.pdf

通常情况下,这适用于程序和数据内存,并且在“正常”的操作系统环境中,操作系统将SHF_ALLOC部分加载到指定地址(忽略或使用非SHF_ALLOC部分作为适当的其他目的),一切准备就绪。
在具有ROM程序存储器的嵌入式环境中,只需要输出映射到ROM地址的SHF_ALLOC部分。
此问题说明了仅根据地址而不考虑SHF_ALLOC的情况是不够的。)
然而:
根据链接器生成ELF文件的方式,初始化数据部分可能需要额外处理。
理想情况下,链接器会产生两个与初始化数据相关的部分:一个在RAM中(运行时数据区),另一个在ROM中(RAM数据的初始化内容)。启动代码引用这些部分的地址,并将init数据复制到RAM区域。在这种情况下,仅在ROM地址范围内输出SHF_ALLOC部分即可自动产生正确结果。
如果链接器代替正常操作系统生成单个部分,则在生成ROM映像时需要识别数据部分并在ROM中的地址进行发射(启动代码需要能够找到该地址)。
大多数好的工具链如果配置正确,则采用前一种方法,但我也曾使用过后者。

1
如果您想要一个模仿 objcopy -O binary 的算法,您需要查看节标题。与通常建议的不同,仅查看段标题是不够的。
来自 objcopy 手册 的引用:

可以使用输出目标为二进制(例如,使用 -O binary)来使用 objcopy 生成原始二进制文件。当 objcopy 生成原始二进制文件时,它将基本上产生输入对象文件内容的内存转储。所有符号和重定位信息将被丢弃。内存转储将从复制到输出文件中的最低部分的加载地址开始。

我认为最后一句话非常明确地说了“节”,因为这确实是发生的事情: objcopy 遍历所有节并丢弃那些不进入原始二进制文件的节。剩余的节按升序地址写入输出文件。输出文件中节之间的空间用零填充。
对于我自己的项目,我需要将 ELF 文件转换为原始二进制文件,而我不想依赖于 objcopy。我想出了以下方法:
  1. 如果一个段的类型是 SHT_NULL 或者 SHT_NOBITS ,那么该段不会被包含在内。这符合 ELF 规范(https://refspecs.linuxbase.org/elf/elf.pdf):
    • SHT_NULL: "该值标记段头为非活动状态;它没有相关联的段。段头的其他成员具有未定义的值。"
    • SHT_NOBITS: "该类型的段在文件中不占用空间,但除此之外与 SHT_PROGBITS 类型相似。虽然该段不包含任何字节,但 sh_offset 成员包含概念上的文件偏移量。"
  2. 如果一个段头的 sh_addr 字段为0,则该段不会被包含在内。引用自 ELF 规范:"如果该段将出现在进程的内存映像中,则该成员给出段的第一个字节应该驻留的地址。否则,该成员包含0。"
  3. 如果一个段头的 sh_size 字段为0,则该段不会被包含在内。引用自 ELF 规范:"该成员以字节为单位给出段的大小。除非段类型是 SHT_NOBITS,否则该段在文件中占据 sh_size 字节。一个类型为 SHT_NOBITS 的段可能具有非零大小,但它不占用文件空间。
  4. 如果一个段头的 sh_flags 字段没有设置 SHF_ALLOC 标志,则该段不会被包含在内。引用自 ELF 规范:"该段在进程执行期间占用内存。一些控制区段不驻留在对象文件的内存映像中;这个属性对于这些区段是关闭的。"

步骤3有些多余,但对我来说很好地过滤出大小为零的段,因此我不需要在稍后的阶段处理这些段。

这对我来说有效,尽管我还没有对它投入很多 ELF 文件。另请注意,这在某种程度上是猜测。我考虑过阅读 objcopy 源代码,但很快就陷入了 libbfd 的深处。


0

我不知道这是否是真正的解决方法,但这就是我最终解决问题的方式:

  # For each program header, get the sections contained by it
  # For each section, calculate the LMA the section will reside at 
  # Do NOT load a section if...
  #   Section type is SHT_NULL or NOBITS
  #   Section size = 0
  #   The LMA is outside of the isr_vector -> header region

请注意,在我的情况下,我有一个截断图像末尾的部分。这使得图像的确切结束位置非常明显。

0

找到所有的程序段:

  • 文件大小非零
  • 类型为PT_LOAD

在这些程序段中找到以下部分:

  • 大小非零
  • 具有节属性标志SHF_ALLOC
  • 不是节类型SHT_NOBITS

这些部分将被连接到输出文件中(从最低地址到最高地址)。默认的间隙填充为0(objcopy可以使用--gap-fill选项更改间隙填充)。对于每个部分,您感兴趣的地址是LMA地址。您需要查看binutils BFD库来弄清楚这一点。请参阅此处

简而言之,以下方法在大多数情况下都有效:

lma_addr = programHdr_physical_addr + sectionHdr_offset - programHdr_offset.

按照LMA地址对各个部分进行排序,然后适当地将它们连接在一起(根据您的应用程序,最可能是0xFF)。
以下是一些使用lief库解析ELF文件的Python代码,根据上述条件过滤出适当的部分,并计算LMA。
# Parse elf file
b = lief.parse(args.elf)
if b is None:
    print(f"File not found: {args.elf}")
    sys.exit(1)

segments = [
    i
    for i in filter(
        lambda x: (x.physical_size > 0)
        and (x.type == lief.ELF.SEGMENT_TYPES.LOAD),
        sorted(b.segments, key=lambda x: x.physical_address),
    )
]

sections = []
for seg in segments:
    sections_temp = [
        i
        for i in filter(
            lambda x: (x.original_size > 0)
            and (x.type != lief.ELF.SECTION_TYPES.NOBITS)
            and (lief.ELF.SECTION_FLAGS.ALLOC in x.flags_list),
            sorted(seg.sections, key=lambda x: x.virtual_address),
        )
    ]
    sections.append(sections_temp)

for idx, seg in enumerate(sections):
    for sec in seg:
        print(
            f"seg={idx},\tsec={sec.name},\tcomputed_lma={hex(segments[idx].physical_address + sec.file_offset - segments[idx].file_offset)}\tseg_paddr={hex(segments[idx].physical_address)},\tsec_vaddr={hex(sec.virtual_address)},\tsec_size={hex(sec.original_size)}"
        )

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接