为什么使用 arm-none-eabi-ld 链接器更改目标文件的顺序会改变可执行行为?

Why does changing the order of object files using the arm-none-eabi-ld linker change the executable behavior?

我在使用

时出现了不同的行为
arm-none-eabi-ld -T t.ld -o t.elf t.o ts.o

到link我的目标文件,vs

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

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

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

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(使用 linker 命令产生预期结果)

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 使用带有转置目标文件的 linker 命令的结果

(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)明显超出了程序的内存范围。

更简单:

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 bootstrap 代码位于地址 0x10000。如果您没有指定部分或对象名称,或者以某种方式告诉链接器专门将某些内容放在那里,那么该工具将按照您在命令行上提供的内容进行处理,并按顺序处理这些内容。因此,如果 bootstrap 代码首先出现在命令行上,那么该入口点将起作用,但如果您先放置其他内容,那么它根本不会起作用,并且理想情况下会以某种方式崩溃。

现在 qemu 允许使用 elf 文件,它可能支持也可能不支持 elf 文件中的入口点,如果您在链接描述文件中指定入口点,这可能会起作用,但当然当您随后采用原始二进制映像版本 (-O binary....... .bin) 版本将在硬件上失败。除非代码是由 elf 加载器或类似的东西(操作系统和像这样支持所有 cr@p 的 sim 环境)加载的,否则只需正确构建文件即可。 (现在了解 cortex-m sims qemu does/did 查看条目的 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>

您可以看到 hello 和 main 交换基于命令行 (.text),但是 .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 名称是您想要的任何名称,您也可以将其称为 banana 而不是 ram、rom 或 flash 或...您看到其他人使用的名称。