Code section in assembly: .text or .code

2022-05-11

Background

前幾天我在閱讀學校開的一門課 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

Debugging

我開始分析出錯的原因,既然錯誤是 segment 有關,而解法是跟 section 有關。那問題一定出在兩種寫法所編譯出來的 ELF 執行檔有差異。

我使用的是 readelf 這個工具來看它們各自的執行檔的 ELF 資訊, 在這邊 minimal_code 代表的是標示 section .code 所編譯出來的執行檔;而 minimal_text 代表的是標示 section .text

Section

指定 -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 是 AXA (SHF_ALLOC) 代表 .text 這個 section 在 runtime 時會佔據記憶體空間 ,而 X (SHF_EXECINSTR) 代表這個 section 包含了可執行的指令。

然而 .code 的 Flags 並沒有包含 X ,代表執行檔不認為 .code section 可以執行!

Segment

再來我們來看執行檔的 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 這個錯誤!

Solution

除了以上將 section .code 改寫成 .section .text 的方法之外,還有沒有其他解決方法呢?

一個最簡單的想法是:既然 .code section 不能執行,我們就修改他的 flags 讓他可以執行就好啦!

這方法雖然可以,但有一點點複雜(可能需要額外在 linker script 內設定屬性之類的)。這裡有一個更簡單的方法是直接將 .code 這個 section 的內容塞進 .text section 就好了!並且 .text 屬於 Special Sections 之一,因此這些屬性自然會包含 EXECUTABLE 的權限。

Customize the linker script

因此我們可以透過自訂我們的 linker script 來做這件事情:(注意:這個 linker script 只適用在此範例程式,例如 data section 根本沒有被處理)

minimal_code.ld:

SECTIONS
{
  . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
  .text :
  {
    *(.code)
  }
}

如果你不懂 linker script ,我稍微介紹一下這個 linker script 在做的事情:

我們可以先看這段程式碼,可以看到 *(.code).text 圍起來的大括號裡面,代表將 .code section 的內容放入 .text section 中:

.text :
{
  *(.code)
}

好的我們介紹完了,這段程式碼主要做的事情就是這樣,如此簡單明瞭!

其中 . = SEGMENT... 是我從預設的 linker script (下 ld --verbose 可以看到) 複製過來的,它的意思就是將程式本體放到 text-segment 的位置,預設是位址 0x400000 + SIZEOF_HEADERSSIZEOF_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 抓來改,這樣就會保證長一樣了)

Appendix

masm

.text 或是 .code 等等 section 的預設屬性是由 assembler 來決定的,例如 yasmnasm 都有描述它們預設 ELF standard sections 的屬性, .code 在兩個 assembler 都不會特別給屬性。

然而在 masm 這個 assembler 中 , .code 的確可以用來標示一段 code 的開始!

引用自 .CODE | Microsoft Docs:

(32-bit MASM only.) When used with .MODEL, indicates the start of a code segment.

.code 或是 .data 這些 directives 在 masm 中稱為 simple segment directives

因此我們可以大膽猜測老師用的是 masm 來編譯範例的程式嗎?不,在投影片上,老師的確是使用 yasm 編譯的。

難道真的是老師寫錯了嗎?

Kernel

後來我在這串討論發現有人跟我遇到一樣的問題。下面的回答除了剛剛說的 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 版本應該沒那麼新所以才覺得這段程式碼沒問題。

Thoughts

一直以來都以為 text segment 和 code segment 這兩個名詞是 interchangable 的。也以為 code section 和 text section 是等價的,所以一開始沒有想到是 section.codesection.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 也可以完全不管,自己想怎麼設定就怎麼設定。

Reference