链接器的仿真与 OUTPUT_FORMAT

Linker's emulation vs OUTPUT_FORMAT

当我将输出格式指定为 i386 时,我的执行得到了一个 SIGSEGV。但是,当我使用 -m elf_i386 选项时,它起作用了。检查手册页,这两个是不同的,因为 OUTPUT_FORMAT 等同于 -oformat 选项。

那么,两者之间有什么区别,在哪些情况下应该使用哪个?

示例代码:

文件hello.c:

int a = 1;
int b;
void _start() {
  /* exit system call */
  asm("movl ,%eax;"
      "xorl %ebx,%ebx;"
      "int  [=11=]x80"
    );
}

script.lds: OUTPUT_FORMAT 和 OUTPUT_ARCH 似乎对我的程序没有任何帮助 运行.

/* OUTPUT_FORMAT("elf32-i386"); */
/* OUTPUT_ARCH(i386); */
OUTPUT(hello);
ENTRY(_start);

SECTIONS
{
  .text 0x10000:
  {
    *(.text)
  }
  .data 0x8000000:
  {
    *(.data)
  }
  .bss :
  {
    *(.bss)
  }
}

命令执行:

gcc -m32 -nostdlib -g -c hello.c -o hello.o
ld -m elf_i386 -T script.lds hello.o

真正的区别在于仿真的意义远不止 OUTPUT_ARCHOUTPUT_FORMAT。一些细节几乎是显而易见的,例如可以通过 --verbose 选项看到的默认链接描述文件的差异,一些在 this document, but most of the answers could only be found in sources, like compare emulation script for elf_i386 and emulation script for elf_x86_64 中进行了描述。差异似乎并没有那么高,但这不是唯一的差异,在您的特定情况下实际咬你的东西甚至无法通过生成之间的 diff 看到(在 ld 构建) ld/eelf_i386.cld/eelf_x86_64.c 文件,因为这归结为来自 bfd 库的常数,并且 取决于仿真。

那么,让我们深入了解一下,看看会发生什么。下面到处都是 script.lds 我的意思是你的脚本有 OUTPUT_ARCHOUTPUT_FORMAT uncommented.

现在,让我们先来看看结果的差异:

$ ld -T script.lds hello.o
$ LC_ALL=C objdump -p hello

hello:     file format elf32-i386

Program Header:
    LOAD off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**21
         filesz 0x00010048 memsz 0x00010048 flags r-x
    LOAD off    0x00200000 vaddr 0x08000000 paddr 0x08000000 align 2**21
         filesz 0x00000004 memsz 0x00000008 flags rw-
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rw-
$ ld -m elf_i386 -T script.lds hello.o
$ LC_ALL=C objdump -p hello

hello:     file format elf32-i386

Program Header:
    LOAD off    0x00001000 vaddr 0x00010000 paddr 0x00010000 align 2**12
         filesz 0x00000048 memsz 0x00000048 flags r-x
    LOAD off    0x00002000 vaddr 0x08000000 paddr 0x08000000 align 2**12
         filesz 0x00000004 memsz 0x00000008 flags rw-
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rw-

注意 "bad" 二进制文件有一个 PT_LOAD 段,虚拟地址为零,对齐方式为 0x00200000。虚拟地址 0 听起来不太对,但让我们看看它为什么真的失败了。调试真的很有趣。如果有人尝试使用 gdb,他会得到这个:

(gdb) run
Starting program: /somewhere/hello 
During startup program terminated with signal SIGSEGV, Segmentation fault.
(gdb) bt
No stack.
(gdb) info registers 
The program has no registers now.

所以程序甚至没有真正启动 运行。再来看strace

$ strace ./hello
execve("./hello", ["./hello"], [/* 108 vars */]) = -1 EPERM (Operation not permitted)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=0} ---
+++ killed by SIGSEGV +++

我们看到 execve() returns EPERM。什么样的许可可能会失败?好吧,这正是因为虚拟地址为零,内核尝试加载我们的 ELF,尝试为虚拟地址 0 映射文件但失败了,因为大约 Linux 2.6.23 次 security feature introduced 禁止这样做。但这可以配置,所以经过简单的

$ echo 0 > /proc/sys/vm/mmap_min_addr

"bad" 二进制文件突然开始工作。

但我们不是要让某些东西在这里工作(耶!),我们是关于 ld 行为的差异。我们的 "bad" 和 "good" 二进制文件之间的不同之处还在于可加载段的对齐方式。如果你想一想,你会发现 ld 行为实际上是绝对正确的,当它具有 0x1000 的对齐约束时,它使用 0x10000 的虚拟地址作为段开始,这对于该对齐是正确的,但是当它有一个 0x200000 的对齐约束,因为我们已经指示它把我们的 .text 放入地址 0x10000 它别无选择,只能使用零的基本虚拟地址!

那么这个对齐要求从何而来?这里我们 return 到我们的仿真东西,因为 elf_i386 和 elf_x86_64 的默认对齐是最大页面大小(通过 bfd_emul_get_maxpagesize() 从 bfd 获得),但是页面大小这些架构不同。

您实际上可以在没有 elf_i386 模拟的情况下构建您的二进制文件,但为此您需要通过参数指定最大页面大小,例如:

$ ld -T script.lds -z max-page-size=0x1000 hello.o

这个生成的二进制文件不仅可以在没有 mmap_min_addr 调整的情况下工作,而且与使用适当的 elf_i386 仿真构建的二进制文件一点一点地相同。

回到最初的问题——细节上的差异是巨大而微妙的。在构建软件时,您肯定希望使用正确的仿真。 99.99% 的情况下,您的 OUTPUT_FORMAT 将与您的仿真参数非常相似。

但是。出色地。有一些情况。你通常不会做的事情。但是如果你小心并且有必要,你可以这样做,例如:

$ head -n 1 script.lds
OUTPUT_FORMAT("srec");
$ ld -T script.lds hello.o 
$ file hello
hello: Motorola S-Record; binary data in text format

确切地说,您的仿真是一回事,而 OUTPUT_FORMAT 实际上是关于出于某些(奇怪的)原因您需要的输出格式。

但是请不要在家里尝试,请使用适当的仿真并忘记所有这些噩梦。