链接器的仿真与 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_ARCH
和 OUTPUT_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.c
和 ld/eelf_x86_64.c
文件,因为这归结为来自 bfd 库的常数,并且 也 取决于仿真。
那么,让我们深入了解一下,看看会发生什么。下面到处都是 script.lds
我的意思是你的脚本有 OUTPUT_ARCH
和 OUTPUT_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
实际上是关于出于某些(奇怪的)原因您需要的输出格式。
但是请不要在家里尝试,请使用适当的仿真并忘记所有这些噩梦。
当我将输出格式指定为 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_ARCH
和 OUTPUT_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.c
和 ld/eelf_x86_64.c
文件,因为这归结为来自 bfd 库的常数,并且 也 取决于仿真。
那么,让我们深入了解一下,看看会发生什么。下面到处都是 script.lds
我的意思是你的脚本有 OUTPUT_ARCH
和 OUTPUT_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
实际上是关于出于某些(奇怪的)原因您需要的输出格式。
但是请不要在家里尝试,请使用适当的仿真并忘记所有这些噩梦。