【问题标题】:Why does changing the order of object files using the arm-none-eabi-ld linker change the executable behavior?为什么使用 arm-none-eabi-ld 链接器更改目标文件的顺序会更改可执行行为?
【发布时间】:2021-12-07 18:39:38
【问题描述】:

使用时我的行为有所不同

arm-none-eabi-ld -T t.ld -o t.elf t.o ts.o

链接我的目标文件,vs

arm-none-eabi-ld -T t.ld -o t.elf ts.o t.o

目标文件 't.o' 和 'ts.o' 在命令中被转置。后一个版本会产生正确的行为,而前一个版本不会。不同之处似乎是我的程序中的堆栈指针与第一个版本设置不正确,我想知道为什么会这样。

这是我正在使用的源文件和链接器脚本,以及要编译的脚本。

t.ld

ENTRY(start) /* define start as the entry address */
SECTIONS
{
    . = 0x10000; /* loading address, required by QEMU */
    .text : { *(.text) }
    .data : { *(.data) }
    .bss : { *(.bss) }
    . =ALIGN(8);
        . =. + 0x1000;
    stack_top =.;
}

t.c

int g = 100; // un-initialized global

extern int sum(int a, int b, int c, int d, int e, int f);

int main() {
    int a, b, c, d, e, f; // local variables
    a = b = c = d = e = f = 1; // values do not matter
    g = sum(a, b, c, d, e, f); // call sum()
}

ts.s

/*
    Assembly file to define sum()
 */
    .global start, sum
start:
    ldr sp, =stack_top // set sp to stack top
    bl main // call main()

stop: b stop // loop

sum:
    // establish stack frame
    stmfd sp!, {fp, lr} // push lr and fp
    add fp, sp, #4 // fp -> saved lr on stack
    // compute sum of all 6 parameters
    add r0, r0, r1 // r0 = a + b
    add r0, r0, r2 // r0 = a + b + c
    add r0, r0, r3 // r0 = a + b + c + d
    ldr r3, [fp, #4] // r1 = e
    add r0, r0, r3 // r0 = a + b + c + d + e
    ldr r3, [fp, #8] // r1 = f
    add r0, r0, r3 // r0 = a + b + c + d + e + f
    // return
    sub sp, fp, #4 // point stack pointer to saved fp
    ldmfd sp!, {fp, pc} // return to caller

mk.sh(带有产生预期结果的链接器命令)

arm-none-eabi-as -o ts.o ts.s # assemble ts.s
arm-none-eabi-gcc -c t.c # cross-compile t.c into t.o
arm-none-eabi-ld -T t.ld -o t.elf ts.o t.o # link object files into t.elf
arm-none-eabi-objcopy -O binary t.elf t.bin # convert t.elf to t.bin

运行二进制文件后

qemu-system-arm -M versatilepb -kernel t.bin -nographic -serial /dev/null

我得到以下信息。堆栈指针(R13)正确

(qemu) info registers
R00=00000000 R01=00000001 R02=000100c0 R03=00000000
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=000110c8 R14=00010008 R15=00010008
PSR=400001d3 -Z-- A svc32
FPSCR: 00000000

VS 使用带有转置目标文件的链接器命令的结果

(qemu) info registers
R00=00000000 R01=00000183 R02=00000100 R03=00000000
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=f3575ee4
R12=00000000 R13=f3575ec0 R14=00010060 R15=00010000
PSR=400001d3 -Z-- A svc32
FPSCR: 00000000

堆栈指针(R13)明显超出程序的内存范围。

【问题讨论】:

  • 它真的找到了你的start 符号吗?如果它只是使用.text 部分的顶部,那将取决于首先链接哪个目标文件。 (但ld 会对此发出警告,例如warning: cannot find entry symbol _start; defaulting to 0000000008049000 或类似的)。您的 .global startENTRY(start) 应该意味着这不是问题,但可能与调试器一起检查它在哪里输入您的代码。
  • 我没有收到您提到的警告,但是鉴于 PC(R15) 仍位于版本不正确的加载地址,我认为您可能是正确的,程序不是事件启动。我没有使用带有 QEMU 裸机的调试器,但我会研究它。感谢您的帮助。
  • 您正在运行 .bin 文件,因此您肯定需要在二进制文件中首先而不是最后一个引导代码 (ts.o),将其他任何内容放在命令行前面将构建一个二进制文件预计不会工作。
  • 哦,对了,你设置了 ELF 入口点就好了,但是你用 objcopy 剥离了元数据,只留下二进制顶部的隐式入口点。我认为这就是 old_timer 的长答案旨在展示的内容,并且可能会在所有文本和代码中的某个地方说出来。
  • 使用 ENTRY() 是 qemu 的一种可能的解决方法,如果您使用 elf 文件,但最好只知道如何使用这些工具。

标签: assembly linker arm qemu arm-none-eabi-gcc


【解决方案1】:

更简单:

flash.s

.global _start
_start:
    ldr sp,=0x11000
    bl main
    b .

flash.ld

ENTRY(_start)

MEMORY
{
    ram : ORIGIN = 0x10000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > ram
    .rodata : { *(.rodata*) } > ram
    .bss    : { *(.bss*)    } > ram
    .data   : { *(.data*)   } > ram
}

so.c

int  main ( void )
{   
    return 5;
}

构建

arm-none-eabi-as --warn --fatal-warnings  flash.s -o flash.o
arm-none-eabi-gcc -c -Wall -O2 -ffreestanding  so.c -o so.o
arm-none-eabi-ld -nostdlib -nostartfiles -T flash.ld flash.o so.o -o one.elf
arm-none-eabi-objdump -D one.elf > one.list
arm-none-eabi-objcopy -O binary one.elf one.bin
arm-none-eabi-ld -nostdlib -nostartfiles -T flash.ld so.o flash.o -o two.elf
arm-none-eabi-objdump -D two.elf > two.list
arm-none-eabi-objcopy -O binary two.elf two.bin

检查:

one.elf:     file format elf32-littlearm


Disassembly of section .text:

00010000 <_start>:
   10000:   e3a0da11    mov sp, #69632  ; 0x11000
   10004:   eb000000    bl  1000c <main>
   10008:   eafffffe    b   10008 <_start+0x8>

0001000c <main>:
   1000c:   e3a00005    mov r0, #5
   10010:   e12fff1e    bx  lr


two.elf:     file format elf32-littlearm


Disassembly of section .text:

00010000 <main>:
   10000:   e3a00005    mov r0, #5
   10004:   e12fff1e    bx  lr

00010008 <_start>:
   10008:   e3a0da11    mov sp, #69632  ; 0x11000
   1000c:   ebfffffb    bl  10000 <main>
   10010:   eafffffe    b   10010 <_start+0x8>

如果您将其作为 .bin 文件运行,则您的 C 引导代码需要位于地址 0x10000。如果您没有指定部分或对象名称,或者以某种方式告诉链接器专门将某些内容放在那里,那么该工具将按照您在命令行上提供的内容进行处理,并按顺序处理这些内容。因此,如果引导代码首先在命令行上,那么该入口点将起作用,但如果您将其他内容放在首位,那么它根本不会起作用,理想情况下会以某种方式崩溃。

现在 qemu 允许使用 elf 文件,它可能支持也可能不支持 elf 文件中的入口点,如果您在链接描述文件中指定入口点,这可能会发生,但当然当您使用原始文件时二进制映像版本(-O 二进制..... .bin)版本,它将在硬件上失败。除非代码是由 elf 加载器或类似的东西(操作系统,像这样支持所有 cr@p 的 sim 环境)加载的,否则只需正确构建文件。 (现在了解 cortex-m sims qemu 确实/确实查看了条目的 lsbit 以正确启动 cortex-m,所以你需要它)。

arm-none-eabi-nm -a one.elf | grep start
00010000 T _start
arm-none-eabi-nm -a two.elf | grep start
00010008 T _start

您应该能够删除上述示例中的 ENTRY 并让 one.bin 正常工作。但是 two.bin 不会。也许使用 ENTRY() two.elf 会起作用,但不是你应该依赖的。

在构建裸机时,您应始终根据硬件(或 sim)检查代码的入口点,以确保在尝试执行二进制文件之前已正确构建它。任何新项目或构建基础架构的任何更改...检查工具链输出。

请注意,如果您正在控制链接描述文件,那么您不需要 _start,即使您不是 (something-ld -Ttext=0x1000 -Tdata=0x2000) 您也不需要它,它可能会发出警告(对于后者),但谁在乎。 _start 被定义为股票链接器脚本中的入口点,一旦您自己制作而不使用股票链接器脚本,您可以根据需要选择入口点的名称和其他名称。

我觉得这很浪费,因为只是让命令行正确是微不足道的,但你会看到人们这样做:

flash.s

.section .init

    ldr sp,=0x11000
    bl main
    b .

.section .text

hello:
    b hello

flash.ld

MEMORY
{
    ram : ORIGIN = 0x10000, LENGTH = 0x1000
}
SECTIONS
{
    .init   : { *(.init*)   } > ram
    .text   : { *(.text*)   } > ram
    .rodata : { *(.rodata*) } > ram
    .bss    : { *(.bss*)    } > ram
    .data   : { *(.data*)   } > ram
}

构建是一样的:

one.elf:     file format elf32-littlearm


Disassembly of section .init:

00010000 <.init>:
   10000:   e3a0da11    mov sp, #69632  ; 0x11000
   10004:   eb000001    bl  10010 <main>
   10008:   eafffffe    b   10008 <hello-0x4>

Disassembly of section .text:

0001000c <hello>:
   1000c:   eafffffe    b   1000c <hello>

00010010 <main>:
   10010:   e3a00005    mov r0, #5
   10014:   e12fff1e    bx  lr

two.elf:     file format elf32-littlearm


Disassembly of section .init:

00010000 <.init>:
   10000:   e3a0da11    mov sp, #69632  ; 0x11000
   10004:   eb000000    bl  1000c <main>
   10008:   eafffffe    b   10008 <main-0x4>

Disassembly of section .text:

0001000c <main>:
   1000c:   e3a00005    mov r0, #5
   10010:   e12fff1e    bx  lr

00010014 <hello>:
   10014:   eafffffe    b   10014 <hello>

您可以看到基于命令行 (.text) 的 hello 和 main 交换,但 .init 是在 .text 之前的链接器脚本中专门调用的。

我觉得这是一个丑陋的 hack,YMMV。一个更丑陋的 hack 是这样的:

flash.s

ldr sp,=0x11000
bl main
b .

flash.ld

MEMORY
{
    ram : ORIGIN = 0x10000, LENGTH = 0x1000
}
SECTIONS
{
    .hello  : { flash.o (.text*)  } > ram
    .text   : { *(.text*)   } > ram
    .rodata : { *(.rodata*) } > ram
    .bss    : { *(.bss*)    } > ram
    .data   : { *(.data*)   } > ram
}

给予:

one.elf:     file format elf32-littlearm


Disassembly of section .hello:

00010000 <.hello>:
   10000:   e3a0da11    mov sp, #69632  ; 0x11000
   10004:   eb000000    bl  1000c <main>
   10008:   eafffffe    b   10008 <main-0x4>

Disassembly of section .text:

0001000c <main>:
   1000c:   e3a00005    mov r0, #5
   10010:   e12fff1e    bx  lr

two.elf:     file format elf32-littlearm


Disassembly of section .hello:

00010000 <.hello>:
   10000:   e3a0da11    mov sp, #69632  ; 0x11000
   10004:   eb000000    bl  1000c <main>
   10008:   eafffffe    b   10008 <main-0x4>

Disassembly of section .text:

0001000c <main>:
   1000c:   e3a00005    mov r0, #5
   10010:   e12fff1e    bx  lr

正如从一开始就提到的:如果您在链接描述文件中明确调用某些内容,它会更改内容,否则它会使用命令行(现在我已经看到了一些例外情况)。在一天结束时,总是在创建新项目或更改构建时检查反汇编,以查看它正在生成可以运行的二进制文件。 (如果地址固定,则入口点在正确的位置,手动组装零件的互通是正确的,等等)。

注意:

.text   : { *(.text*)   } > ram

左侧的 .text 名称是您想要的任何名称,大多数人保留该名称,因为它以传统方式表示某些意思,但您可以在左侧命名这些您想要的名称。编译器使用 .text、.bss、.data 或其他格式,因此您必须正确获取右侧。

MEMORY
{
    ram : ORIGIN = 0x10000, LENGTH = 0x1000
}
SECTIONS
{
    .hello  : { flash.o (.text*)  } > ram
    .world  : { *(.text*)   } > ram
}

Disassembly of section .hello:

00010000 <.hello>:
   10000:   e3a0da11    mov sp, #69632  ; 0x11000
   10004:   eb000000    bl  1000c <main>
   10008:   eafffffe    b   10008 <main-0x4>

Disassembly of section .world:

0001000c <main>:
   1000c:   e3a00005    mov r0, #5
   10010:   e12fff1e    bx  lr

nm 和 readelf 和其他人对此很好。诸如操作系统之类的加载器工具或带有 elf 文件的 qemu 可能希望也可能不希望查看 .bss、.data 等...必须根据具体情况进行处理。大多数人只是使用传统名称。

请注意,内存部分的 ram 名称是您想要的任何名称,您也可以将其命名为香蕉,而不是您看到其他人使用的 ram 或 rom 或 flash 或 ...。

【讨论】:

  • 感谢您提供了非常丰富的答复,您回答了我的问题并引导我朝着正确的方向前进,我非常感谢!
猜你喜欢
  • 1970-01-01
  • 2017-03-24
  • 1970-01-01
  • 1970-01-01
  • 2017-10-10
  • 1970-01-01
  • 2016-05-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多