在 ELF 中,为什么 headers 需要在一个段中?
In ELF, why do the headers need to be in one segment?
出于学习目的,我制作了这个简单的 ELF:
bits 64
org 0x08048000
elfHeader:
db 0x7F, "ELF", 2, 1, 1, 0 ; e_ident
db 0 ; abi version
times 7 db 0 ; unused padding
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw elfHeaderSize ; e_ehsize
dw programHeaderSize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
elfHeaderSize equ $ - elfHeader
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
dq 0 ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq fileSize ; p_filesz
dq fileSize ; p_memsz
dq 0x1000 ; p_align
programHeaderSize equ $ - programHeader
_start:
xor rdi, rdi
xor eax,eax
mov al,60
syscall
fileSize equ $ - $$
为了编译我使用的代码 NASM:
nasm -f bin exe.asm -o exe
如果你看一下 programHeader
,你会看到 p_offset
是 0,而 p_filesz
是 fileSize
。这意味着该段包含整个文件。这是我没有预料到的(and I'm not the only one),但显然 Linux 操作系统需要 headers 位于 PT_LOAD
类型的段中,以便加载信息。
这是我能找到的唯一提到 headers 在一个段内这一事实的资源:https://www.intezer.com/blog/research/executable-linkable-format-101-part1-sections-segments/
Something important to highlight about segments is that only PT_LOAD segments get loaded into memory. Therefore, every other segment is mapped within the memory range of one of the PT_LOAD segments.
In order to understand the relationship between Sections and Segments, we can picture segments as a tool to make the linux loader’s life easier, as they group sections by attributes into single segments in order to make the loading process of the executable more efficient, instead of loading each individual section into memory. The following diagram attempts to illustrate this concept:
但我不明白为什么 Linux 需要 headers 在 运行 时加载。它们的用途是什么?如果进程需要它们运行,难道不能Linux自己加载吗?
编辑:
评论中已经提到 headers 不需要加载,但是,有时无论如何都会加载它们以避免必须添加填充。我尝试添加填充以使其对齐 4KB,但没有用。这是我的尝试:
bits 64
org 0x08048000
elfHeader:
db 0x7F, "ELF", 2, 1, 1, 0 ; e_ident
db 0 ; abi version
times 7 db 0 ; unused padding
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw elfHeaderSize ; e_ehsize
dw programHeaderSize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
elfHeaderSize equ $ - elfHeader
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
dq _start - $$ ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq codeSize ; p_filesz
dq codeSize ; p_memsz
dq 0x1000 ; p_align
programHeaderSize equ $ - programHeader
; padding until 4KB
paddingUntil4k equ 4*1024 - ($ - elfHeader)
times paddingUntil4k db 0
_start:
xor rdi, rdi
xor eax,eax
mov al,60
syscall
codeSize equ $ - _start
fileSize equ $ - $$
But I don't understand why Linux needs that headers to be loaded at run time.
它不会。
What are they used for? If they are needed for the process to run, couldn't Linux load it by himself?
要回答所有这些问题,您需要查看 Linux 内核源代码。
在 the source 中,您可以看到实际上程序 header 做 而不是 需要成为任何 PT_LOAD
段的一部分,并且内核将自行读取它们。
像这样更改您的原始程序:
diff -u exe.asm.orig exe.asm
--- exe.asm.orig 2021-02-07 18:54:34.449336515 -0800
+++ exe.asm 2021-02-07 18:53:19.773532451 -0800
@@ -24,9 +24,9 @@
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
- dq 0 ; p_offset
- dq $$ ; p_vaddr
- dq $$ ; p_paddr
+ dq _start - $$ ; p_offset
+ dq _start ; p_vaddr
+ dq _start ; p_paddr
dq fileSize ; p_filesz
dq fileSize ; p_memsz
dq 0x1000 ; p_align
生成一个 运行 没问题的程序,但其中的程序 header 不在 PT_LOAD
段中:
eu-readelf --all exe
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Ident Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AMD x86-64
Version: 1 (current)
Entry point address: 0x8048078
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags:
Size of this header: 64 (bytes)
Size of program header entries: 56 (bytes)
Number of program headers entries: 1
Size of section header entries: 0 (bytes)
Number of section headers entries: 0 ([0] not available)
Section header string table index: 0
Section Headers:
[Nr] Name Type Addr Off Size ES Flags Lk Inf Al
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000078 0x0000000008048078 0x0000000008048078 0x000081 0x000081 RWE 0x1000
I have tried adding padding
你没有做对。使用您的“带填充”源会产生以下 exe-padding
:
...
Entry point address: 0x8049000
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000
这个二进制文件由内核启动,并立即跳转到起始地址0x8049000
,没有映射(因为它没有被PT_LOAD
段),导致立即SIGSEGV
.
要解决这个问题,您需要调整入口地址:
diff -u exe-padding.asm.orig exe-padding.asm
--- exe-padding.asm.orig 2021-02-07 18:57:31.800871195 -0800
+++ exe-padding.asm 2021-02-07 19:34:27.303071700 -0800
@@ -8,7 +8,7 @@
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
- dq _start ; e_entry
+ dq _start - 0x1000 ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
这再次生成了一个可运行的可执行文件。备案:
eu-readelf --all exe-padding
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Ident Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AMD x86-64
Version: 1 (current)
Entry point address: 0x8048000
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags:
Size of this header: 64 (bytes)
Size of program header entries: 56 (bytes)
Number of program headers entries: 1
Size of section header entries: 0 (bytes)
Number of section headers entries: 0 ([0] not available)
Section header string table index: 0
Section Headers:
[Nr] Name Type Addr Off Size ES Flags Lk Inf Al
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000
P.S。您正在 0x08048000
链接您的 64 位程序,这是 i*86
(32 位)可执行文件的传统加载地址。 x86_64
二进制文件通常从 0x400000
.
开始
更新:
About the first example, p_filesz is still fileSize, I think that should get outside of the boundaries of the file.
这是正确的:p_filesz
和 p_memsz
应该减少 header 的大小(这里是 0x78
)。请注意,这两者都将四舍五入为页面大小(在添加 p_offset
之后),因此对于此示例,没有实际差异。
更新二:
pastebin.ubuntu.com/p/rgfVMrbcmJ
这导致以下 LOAD
段:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000078 0x0000000008048000 0x0000000008048000 0x000081 0x000081 RWE 0x1000
这个二进制文件不会 运行(内核会拒绝它),因为它要求内核做不可能的事情:将 mmap
字节偏移 0x78
到页面开始。
如果应用程序执行等效的 mmap
调用,它会得到 EINVAL
错误,因为 mmap
要求 (offset % pagesize) == (addr % pagesize)
.
出于学习目的,我制作了这个简单的 ELF:
bits 64
org 0x08048000
elfHeader:
db 0x7F, "ELF", 2, 1, 1, 0 ; e_ident
db 0 ; abi version
times 7 db 0 ; unused padding
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw elfHeaderSize ; e_ehsize
dw programHeaderSize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
elfHeaderSize equ $ - elfHeader
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
dq 0 ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq fileSize ; p_filesz
dq fileSize ; p_memsz
dq 0x1000 ; p_align
programHeaderSize equ $ - programHeader
_start:
xor rdi, rdi
xor eax,eax
mov al,60
syscall
fileSize equ $ - $$
为了编译我使用的代码 NASM:
nasm -f bin exe.asm -o exe
如果你看一下 programHeader
,你会看到 p_offset
是 0,而 p_filesz
是 fileSize
。这意味着该段包含整个文件。这是我没有预料到的(and I'm not the only one),但显然 Linux 操作系统需要 headers 位于 PT_LOAD
类型的段中,以便加载信息。
这是我能找到的唯一提到 headers 在一个段内这一事实的资源:https://www.intezer.com/blog/research/executable-linkable-format-101-part1-sections-segments/
Something important to highlight about segments is that only PT_LOAD segments get loaded into memory. Therefore, every other segment is mapped within the memory range of one of the PT_LOAD segments.
In order to understand the relationship between Sections and Segments, we can picture segments as a tool to make the linux loader’s life easier, as they group sections by attributes into single segments in order to make the loading process of the executable more efficient, instead of loading each individual section into memory. The following diagram attempts to illustrate this concept:
但我不明白为什么 Linux 需要 headers 在 运行 时加载。它们的用途是什么?如果进程需要它们运行,难道不能Linux自己加载吗?
编辑:
评论中已经提到 headers 不需要加载,但是,有时无论如何都会加载它们以避免必须添加填充。我尝试添加填充以使其对齐 4KB,但没有用。这是我的尝试:
bits 64
org 0x08048000
elfHeader:
db 0x7F, "ELF", 2, 1, 1, 0 ; e_ident
db 0 ; abi version
times 7 db 0 ; unused padding
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw elfHeaderSize ; e_ehsize
dw programHeaderSize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
elfHeaderSize equ $ - elfHeader
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
dq _start - $$ ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq codeSize ; p_filesz
dq codeSize ; p_memsz
dq 0x1000 ; p_align
programHeaderSize equ $ - programHeader
; padding until 4KB
paddingUntil4k equ 4*1024 - ($ - elfHeader)
times paddingUntil4k db 0
_start:
xor rdi, rdi
xor eax,eax
mov al,60
syscall
codeSize equ $ - _start
fileSize equ $ - $$
But I don't understand why Linux needs that headers to be loaded at run time.
它不会。
What are they used for? If they are needed for the process to run, couldn't Linux load it by himself?
要回答所有这些问题,您需要查看 Linux 内核源代码。
在 the source 中,您可以看到实际上程序 header 做 而不是 需要成为任何 PT_LOAD
段的一部分,并且内核将自行读取它们。
像这样更改您的原始程序:
diff -u exe.asm.orig exe.asm
--- exe.asm.orig 2021-02-07 18:54:34.449336515 -0800
+++ exe.asm 2021-02-07 18:53:19.773532451 -0800
@@ -24,9 +24,9 @@
programHeader:
dd 1 ; p_type
dd 7 ; p_flags
- dq 0 ; p_offset
- dq $$ ; p_vaddr
- dq $$ ; p_paddr
+ dq _start - $$ ; p_offset
+ dq _start ; p_vaddr
+ dq _start ; p_paddr
dq fileSize ; p_filesz
dq fileSize ; p_memsz
dq 0x1000 ; p_align
生成一个 运行 没问题的程序,但其中的程序 header 不在 PT_LOAD
段中:
eu-readelf --all exe
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Ident Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AMD x86-64
Version: 1 (current)
Entry point address: 0x8048078
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags:
Size of this header: 64 (bytes)
Size of program header entries: 56 (bytes)
Number of program headers entries: 1
Size of section header entries: 0 (bytes)
Number of section headers entries: 0 ([0] not available)
Section header string table index: 0
Section Headers:
[Nr] Name Type Addr Off Size ES Flags Lk Inf Al
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000078 0x0000000008048078 0x0000000008048078 0x000081 0x000081 RWE 0x1000
I have tried adding padding
你没有做对。使用您的“带填充”源会产生以下 exe-padding
:
...
Entry point address: 0x8049000
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000
这个二进制文件由内核启动,并立即跳转到起始地址0x8049000
,没有映射(因为它没有被PT_LOAD
段),导致立即SIGSEGV
.
要解决这个问题,您需要调整入口地址:
diff -u exe-padding.asm.orig exe-padding.asm
--- exe-padding.asm.orig 2021-02-07 18:57:31.800871195 -0800
+++ exe-padding.asm 2021-02-07 19:34:27.303071700 -0800
@@ -8,7 +8,7 @@
dw 2 ; e_type
dw 62 ; e_machine
dd 1 ; e_version
- dq _start ; e_entry
+ dq _start - 0x1000 ; e_entry
dq programHeader - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
这再次生成了一个可运行的可执行文件。备案:
eu-readelf --all exe-padding
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Ident Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AMD x86-64
Version: 1 (current)
Entry point address: 0x8048000
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags:
Size of this header: 64 (bytes)
Size of program header entries: 56 (bytes)
Number of program headers entries: 1
Size of section header entries: 0 (bytes)
Number of section headers entries: 0 ([0] not available)
Section header string table index: 0
Section Headers:
[Nr] Name Type Addr Off Size ES Flags Lk Inf Al
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x0000000008048000 0x0000000008048000 0x000009 0x000009 RWE 0x1000
P.S。您正在 0x08048000
链接您的 64 位程序,这是 i*86
(32 位)可执行文件的传统加载地址。 x86_64
二进制文件通常从 0x400000
.
更新:
About the first example, p_filesz is still fileSize, I think that should get outside of the boundaries of the file.
这是正确的:p_filesz
和 p_memsz
应该减少 header 的大小(这里是 0x78
)。请注意,这两者都将四舍五入为页面大小(在添加 p_offset
之后),因此对于此示例,没有实际差异。
更新二:
pastebin.ubuntu.com/p/rgfVMrbcmJ
这导致以下 LOAD
段:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000078 0x0000000008048000 0x0000000008048000 0x000081 0x000081 RWE 0x1000
这个二进制文件不会 运行(内核会拒绝它),因为它要求内核做不可能的事情:将 mmap
字节偏移 0x78
到页面开始。
如果应用程序执行等效的 mmap
调用,它会得到 EINVAL
错误,因为 mmap
要求 (offset % pagesize) == (addr % pagesize)
.