问题描述
简介
我正在编译一个简单的汇编代码(Intel 语法、x86、Linux),打印“Hello World!”。这是:
SECTION .rodata
msg: db 'Hello World!',0xA
msg_len: equ $ - msg
SECTION .text
global _start
_start:
mov eax,4 ; `write` system call
mov ebx,1 ; to stdout
mov ecx,msg
mov edx,msg_len
int 0x80
mov eax,1 ; `exit` system call
xor ebx,ebx ; exit code 0
int 0x80
我使用以下命令编译它:
nasm -f elf32 -o hello_world.o hello_world.s
ld -m elf_i386 -o hello_world hello_world.o
-rwxrwxr-x 1 nikolay nikolay 8704 Apr 27 15:20 hello_world
-rw-rw-r-- 1 nikolay nikolay 243 Apr 26 22:16 hello_world.s
-rw-rw-r-- 1 nikolay nikolay 640 Apr 27 15:20 hello_world.o
问题
目标文件比源代码稍大,但看起来合理,因为ELF文件中应该有一些元数据或其他东西,源代码不包含,对吧?但是可执行文件比目标文件还要大10倍以上!
此外,目标文件中有一些零字节,但我不会说它们太多。但是,可执行文件中有很多零(请参阅 Additional info
部分中两者的屏幕截图)。
调查
我曾尝试阅读一些关于 ELF 的文章,包括维基百科和手册页。我没有仔细阅读它们,所以我可能错过了一些东西,但我发现有用的是 dumpelf
实用程序(来自 pax-utils
包,可通过 apt
安装),使用它我转储了我的精灵文件并发现了可能是这些零流的原因:
在可执行文件的所有三个标头中,都有 p_align
字段集:
.p_align = 4096,/* (min mem alignment in bytes) */
这应该意味着每个部分都应该用零字节填充,以便其长度是 4096 的倍数。并且由于以下每个部分的大小相对较小,因此需要添加很多零字节,这就是这些零的来源。
问题
所以,我想知道:
-
我说得对吗?添加这些零字节是为了使部分足够长吗?
我还注意到前三个部分(
''
、'.rodata'
、'.text'
)从0
、4096
和8192
开始分别,但以下 ('.symtab'
,'.strtab'
,'.shstrtab'
) 似乎不再对齐:它们开始于8208
、8368
和 {{1 }}... 为什么?这里发生了什么? -
我们需要这种对齐方式有什么用?在编程头中,有
8422
和p_vaddr
字段被设置为前三个部分开始的地址,那么如果我们已经知道 exact 来自标题的部分的地址?它与内存页(在我的机器上大小为 4KiB)有关吗? -
我什么时候想要/需要,以及如何更改对齐值?看起来应该有一个链接器参数来更改此值。我在
p_paddr
的手册中找到了--nmagic
参数,它完全禁用了对齐(并且,万岁!,可执行文件与目标文件的大小相同),但我猜对齐是故意存在的,所以也许我只需要减少该值以使其更适合我的情况?
如果您知道我遗漏了什么,我非常感谢您回答这些问题中的任何一个或任何其他细节。也请告诉我我是否在任何地方错了。提前致谢!
其他信息
我的目标文件的转储(带有 ld
):
我的可执行文件转储的一部分(使用类似于上面的命令): 一个新的部分从地址 0x1000
开始xxd hello_world.o | grep -E '0000|$' --color=always | less -R
的输出:
dumpelf hello_world.o
#include <elf.h>
/*
* ELF dump of 'hello_world.o'
* 640 (0x280) bytes
*/
Elf32_Dyn dumpedelf_dyn_0[];
struct {
Elf32_Ehdr ehdr;
Elf32_Phdr phdrs[0];
Elf32_Shdr shdrs[7];
Elf32_Dyn *dyns;
} dumpedelf_0 = {
.ehdr = {
.e_ident = { /* (EI_NIDENT bytes) */
/* [0] EI_MAG: */ 0x7F,'E','L','F',/* [4] EI_CLASS: */ 1,/* (ELFCLASS32) */
/* [5] EI_DATA: */ 1,/* (ELFDATA2LSB) */
/* [6] EI_VERSION: */ 1,/* (EV_CURRENT) */
/* [7] EI_OSABI: */ 0,/* (ELFOSABI_NONE) */
/* [8] EI_ABIVERSION: */ 0,/* [9-15] EI_PAD: */ 0x0,0x0,},.e_type = 1,/* (ET_REL) */
.e_machine = 3,/* (EM_386) */
.e_version = 1,/* (EV_CURRENT) */
.e_entry = 0x0,/* (start address at runtime) */
.e_phoff = 0,/* (bytes into file) */
.e_shoff = 64,/* (bytes into file) */
.e_flags = 0x0,.e_ehsize = 52,/* (bytes) */
.e_phentsize = 0,/* (bytes) */
.e_phnum = 0,/* (program headers) */
.e_shentsize = 40,/* (bytes) */
.e_shnum = 7,/* (section headers) */
.e_shstrndx = 3
},.phdrs = {
/* no program headers ! */ },.shdrs = {
/* Section Header #0 '' 0x40 */
{
.sh_name = 0,.sh_type = 0,/* [SHT_NULL] */
.sh_flags = 0,.sh_addr = 0x0,.sh_offset = 0,/* (bytes) */
.sh_size = 0,/* (bytes) */
.sh_link = 0,.sh_info = 0,.sh_addralign = 0,.sh_entsize = 0
},/* Section Header #1 '.rodata' 0x68 */
{
.sh_name = 1,.sh_type = 1,/* [SHT_PROGBITS] */
.sh_flags = 2,.sh_offset = 352,/* (bytes) */
.sh_size = 13,.sh_addralign = 4,/* Section Header #2 '.text' 0x90 */
{
.sh_name = 9,/* [SHT_PROGBITS] */
.sh_flags = 6,.sh_offset = 368,/* (bytes) */
.sh_size = 31,.sh_addralign = 16,/* Section Header #3 '.shstrtab' 0xB8 */
{
.sh_name = 15,.sh_type = 3,/* [SHT_STRTAB] */
.sh_flags = 0,.sh_offset = 400,/* (bytes) */
.sh_size = 51,.sh_addralign = 1,/* Section Header #4 '.symtab' 0xE0 */
{
.sh_name = 25,.sh_type = 2,/* [SHT_SYMTAB] */
.sh_flags = 0,.sh_offset = 464,/* (bytes) */
.sh_size = 112,/* (bytes) */
.sh_link = 5,.sh_info = 6,.sh_entsize = 16
},/* Section Header #5 '.strtab' 0x108 */
{
.sh_name = 33,.sh_offset = 576,/* (bytes) */
.sh_size = 37,/* Section Header #6 '.rel.text' 0x130 */
{
.sh_name = 41,.sh_type = 9,/* [SHT_REL] */
.sh_flags = 0,.sh_offset = 624,/* (bytes) */
.sh_size = 8,/* (bytes) */
.sh_link = 4,.sh_info = 2,.sh_entsize = 8
},.dyns = dumpedelf_dyn_0,};
Elf32_Dyn dumpedelf_dyn_0[] = {
/* no dynamic tags ! */ };
的输出:
dumpelf hello_world
解决方法
对齐是4096字节,这是这个架构上的页面大小。这并非巧合,正如 man page 关于 nmagic 所说:“关闭部分的页面对齐”。
通过正常(非 nmagic)二进制文件的大小,您可以猜测链接器布置了三个页面,大概具有不同的访问权限(代码 = 不可写,数据 = 不可执行,rodata = 只读),这些权限只能每页设置。运行时,磁盘布局与 RAM 中的布局匹配。
这对于需求分页很重要。程序启动时,整个可执行文件基本上都被mmaped了,并根据需要通过页面错误从磁盘加载页面。此外,页面可以在其其他运行实例之间共享(这对于动态库更重要),并且可以在需要时由于内存压力而从 RAM 中驱逐。
nmagic 可执行文件在运行时仍被加载到三个页面中,但由于那些不再与磁盘上的内容匹配,因此它不是按需分页的。我不建议在更大的物体上使用该选项。
注意:如果您制作了一个运行时间更长的可执行文件(可能添加了对输入的读取),您可以通过查看 /proc/[pid]/maps 和 smaps 来检查正在运行的进程的内存布局细节。