2022-05-11
前幾天我在閱讀學校開的一門課 Advanced Programming in the UNIX Environment 的上課投影片,其中展示了一個用 x86 assembly 寫的範例程式:
; Compile with: yasm -f elf64 -DYASM -D__x86_64__ -DPIC minimal.asm -o minimal.o
; Link with: ld -m elf_x86_64 -o minimal minimal.o
global _start:function
section .code
_start:
mov rax, 60
xor rdi, rdi
syscall
我試著用投影片上面的指令編譯並執行,但一執行就跳出 segmentation fault 的錯誤:
zsh: segmentation fault (core dumped) ./minimal
接著我試著用 gdb 來除錯,卻發現它執行到 _start
的第一行
mov rax, 60
就出錯了,依然看不出什麼端倪:
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401000 in _start ()
這時的我很納悶,又接著編譯執行了投影片上寫的其他範例程式,但是都是一樣的狀況,發現都是執行到第一行就出錯了。
我開始上網搜尋 assembly
的範例,重複比對其他人的程式碼和我的,終於發現只要把
section .code
改成 section .text
就可以成功跑了。
; Compile with: yasm -f elf64 -DYASM -D__x86_64__ -DPIC minimal.asm -o minimal.o
; Link with: ld -m elf_x86_64 -o minimal minimal.o
global _start:function
section .text
_start:
mov rax, 60
xor rdi, rdi
syscall
我開始分析出錯的原因,既然錯誤是 segment 有關,而解法是跟 section 有關。那問題一定出在兩種寫法所編譯出來的 ELF 執行檔有差異。
我使用的是 readelf
這個工具來看它們各自的執行檔的 ELF
資訊, 在這邊 minimal_code
代表的是標示
section .code
所編譯出來的執行檔;而
minimal_text
代表的是標示 section .text
。
指定 -S
flag 代表觀察執行檔的 section headers 資訊。
minimal_text
:
$ readelf -S minimal_text
There are 5 section headers, starting at offset 0x10f0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000401000 00001000
000000000000000c 0000000000000000 AX 0 0 16
# ......
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
# ......
minimal_code
:
$ readelf -S minimal_code
There are 5 section headers, starting at offset 0x10f0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .code PROGBITS 0000000000401000 00001000
000000000000000c 0000000000000000 A 0 0 1
# ......
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
# ......
在此我們不多介紹 Section Headers 各欄位的意義,我們只專注於 Flags
這個欄位,其代表該 section 的屬性,可以看到 .text
的 Flags
是 AX
。 A
(SHF_ALLOC
) 代表
.text
這個 section 在 runtime 時會佔據記憶體空間 ,而
X
(SHF_EXECINSTR
) 代表這個 section
包含了可執行的指令。
然而 .code
的 Flags 並沒有包含 X
,代表執行檔不認為 .code
section 可以執行!
再來我們來看執行檔的 segment 資訊, readelf
指定
-l
flag 觀察 segments 的資訊。
minimal_text
:
$ readelf -l minimal_text
Elf file type is EXEC (Executable file)
Entry point 0x401000
There are 2 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000b0 0x00000000000000b0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x000000000000000c 0x000000000000000c R E 0x1000
Section to Segment mapping:
Segment Sections...
00
01 .text
minimal_code
:
Elf file type is EXEC (Executable file)
Entry point 0x401000
There is 1 program header, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000100c 0x000000000000100c R 0x1000
Section to Segment mapping:
Segment Sections...
00 .code
在此我們也只專注在 Segment 的 Flags 欄位以及下方的 “Section to Segment mapping” 資訊。 “Section to Segment mapping” 顧名思議就是描述了哪個 segment 包含了哪些 sections ,其中 Segment 的數字就是從上方的 Program Headers 從上數下來的第幾個 Segment。
可以看到在 minimal_text
中,包含 .text
section 的是 01 segment ,也就是第二個 segment ,其 Flags 是
R
(PF_R
) 和 E
(PF_E
)
,代表此 segment 可讀可執行。
然而在包含 .code
的 section 中,我們卻看不到有 Flags
有包含 E
,因此代表著此 segment 不可執行,也難怪會有
Segmentation fault 這個錯誤!
除了以上將 section .code
改寫成
.section .text
的方法之外,還有沒有其他解決方法呢?
一個最簡單的想法是:既然 .code
section
不能執行,我們就修改他的 flags 讓他可以執行就好啦!
這方法雖然可以,但有一點點複雜(可能需要額外在 linker script
內設定屬性之類的)。這裡有一個更簡單的方法是直接將 .code
這個 section 的內容塞進 .text
section 就好了!並且
.text
屬於 Special
Sections 之一,因此這些屬性自然會包含 EXECUTABLE 的權限。
因此我們可以透過自訂我們的 linker script 來做這件事情:(注意:這個 linker script 只適用在此範例程式,例如 data section 根本沒有被處理)
minimal_code.ld
:
如果你不懂 linker script ,我稍微介紹一下這個 linker script 在做的事情:
我們可以先看這段程式碼,可以看到 *(.code)
在
.text
圍起來的大括號裡面,代表將 .code
section
的內容放入 .text
section 中:
好的我們介紹完了,這段程式碼主要做的事情就是這樣,如此簡單明瞭!
其中 . = SEGMENT...
是我從預設的 linker script (下
ld --verbose
可以看到)
複製過來的,它的意思就是將程式本體放到 text-segment 的位置,預設是位址
0x400000 + SIZEOF_HEADERS
, SIZEOF_HEADER
是
program header 大小 。
接著重新做 linking ,這邊指定 ld
的
--script
flag 來使用我們自己定義的 linker script (預設的的
linker script 就不會使用了) :
$ ld -m elf_x86_64 --script=minimal_code.ld -o minimal_code minimal_code.o
這時嘗試跑我們的 minimal_code
,會發現沒有
segmentation fault
了!可喜可賀!
$ ./minimal_code
$
我們再次使用 readelf
來觀察一下新的
minimal_code
的 section 與 segment 資訊:
Section:
$ readelf -S minimal_code
There are 5 section headers, starting at offset 0x148:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 00000000004000b0 000000b0
0000000000000010 0000000000000000 AX 0 0 16
Segment:
$ readelf -l minimal_code
Elf file type is EXEC (Executable file)
Entry point 0x401000
There are 2 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000b0 0x00000000000000b0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x000000000000000c 0x000000000000000c R E 0x1000
Section to Segment mapping:
Segment Sections...
00
01 .text
可以發現已經完全看不到 .code
section 的蹤影了!而且跟
minimal_text
吐出的資訊長得是幾乎一模模一樣樣!但不一樣也很正常,畢竟我們有很多額外的設定是我們的腳本沒考慮到的。(當然你也可以直接將預設的
linker script 抓來改,這樣就會保證長一樣了)
.text
或是 .code
等等 section
的預設屬性是由 assembler 來決定的,例如 yasm
和 nasm
都有描述它們預設 ELF standard sections 的屬性, .code
在兩個 assembler 都不會特別給屬性。
然而在 masm
這個 assembler 中 , .code
的確可以用來標示一段 code 的開始!
(32-bit MASM only.) When used with .MODEL, indicates the start of a code segment.
.code
或是 .data
這些 directives 在
masm
中稱為 simple segment directives 。
因此我們可以大膽猜測老師用的是 masm
來編譯範例的程式嗎?不,在投影片上,老師的確是使用 yasm
編譯的。
難道真的是老師寫錯了嗎?
後來我在這串討論發現有人跟我遇到一樣的問題。下面的回答除了剛剛說的
masm
之外,還發現到是跟 Linux kernel 版本有關係。如果使用的
kernel 版本在 5.8 以上、64-bit 執行檔且沒有開 NX (No-eXecute)
防護,若沒有 PT_GNU_STACK
這個 section 時,預設就會將非
.text
section 的屬性設成不可執行。反之如果在 5.8
以下就是可執行的。
然而我們的範例程式是用 yasm
編出來的,沒有
gcc
預設的 NX 防護,也沒有 PT_GNU_STACK
這個
segment ,且我的 kernel 版本高於
5.8,因此才會造成執行錯誤的狀況發生。估計老師在寫這段程式碼的時候,他的環境
kernel 版本應該沒那麼新所以才覺得這段程式碼沒問題。
一直以來都以為 text segment 和 code segment 這兩個名詞是
interchangable 的。也以為 code section 和 text section
是等價的,所以一開始沒有想到是 section.code
和
section.text
的問題,更何況在網路上甚至有解答分明是在誤導:
引用自 assembly - difference between .text and .code section name - Stack Overflow :
There is no functional difference between the
.text
and.code
sections of a binary.In almost all cases, they are completely synonymous (meaning that they refer to the same section), but even when they’re not ( e.g. due to the actual order of sections in the binary), they are semantically identical.
但這次踩雷後也學到了很多東西,例如 ELF 格式、如何寫 linker script 等等。也體認到 ELF format 只不過是一個標準規範而已,真正實作還是要看 OS,OS 也可以完全不管,自己想怎麼設定就怎麼設定。