Linux: 用Live CD跑硬碟裏面的Linux

我用這個救過過兩次grub GG的狀況,每次都要上網回想一下,趁著還記得的情況紀錄一下。

  • 用Live CD開機。你的Live CD要和你硬碟的Linux distribution相同。
  • 想辦法開終端機
  • 輸入以下的咒語指令,假設你的硬碟root filesystem partition放在/dev/sda2
sudo mount /dev/sda2 /mnt
sudo mount --bind /dev /mnt/dev
sudo mount --bind /dev/pts /mnt/dev/pts
sudo mount --bind /proc /mnt/proc
sudo mount --bind /sys /mnt/sys
sudo chroot /mnt

剩下就是處理grub問題了,請自己估狗。

注意!grub是bootloader,沒弄好有可能會讓你的硬碟資料GG。操作有風險,操作前應詳閱相關文件資料。如果你不知道boot loader,grub,root file system,mount,chroot是什麼的話,請找熟悉的碰友幫你處理。

閒聊:忍耐力

三四年前我聽過關於Linux下面中文輸入法的技術演講。技術內容忘得一乾二淨,然而印象最深刻的事講者說台灣人忍耐力太強,所以台灣中文輸入法就變成堪用,進度速度不夠快。
 
私自推論忍耐力太強的習性是造成問題長時間存在、把人類當成猴子用的悲劇原因之一。而且忍耐力強的話,到後面會把察覺到問題的能力降低或是消滅掉。我會這樣比喻:肚子痛想說忍耐一下。過幾天又在痛,想說大概最近吃到不乾淨的東西,接下來都這樣認為。直到幾個月後有天昏倒送醫才發現這是重病的症狀,而到那時候已經很難處理了。

雖然後來後來還是沒去幫忙看程式碼(組裝工程度太差又沒耐性,大大對不起)。但是至少我會比較願意看到問題讓開發者或是負責人知道,而不是單純地認為在找別人麻煩。雖然有沒有改善並不是我該關心的,但是也許作者或是負責人他們真的沒注意到這塊,這樣來說,你是幫作者或是負責人和自己的忙。

私密景點

以前出遊和當地居民聊天聽到兩個故事。

第一個是在南部,一位年輕人帶我去私密景點。這個景點是個天然池塘,生態豐富,但是水草叢生,所以蚊子也不少。他說以前他們曾經有整理場地,把水草清掉,變得十分美麗。雖然沒有宣傳,但是過了中秋節以後,這地方就出現一大堆垃圾,經過清理後過一陣子水面還可以看到油光。所以他們後來就不去整理,讓雜草長回來。

另外一個在北部靠近山區,一位大哥跟我說剛解嚴時期,他跑去以前封鎖的海邊逛,發現到處都有毛蟹,他帶了幾個回去料理,後來他再回去海岸,除了垃圾,什麼都不剩了。

聽完這兩個故事,我決定把私密景點留在我的腦海中就好。希望那天國民水準可以提升到尊重土地,尊重生態,尊重自然。

對作者仁慈就是對讀者殘忍

沒人看得懂的文章,或是要用力猜測才能理解的文章,就是一篇失敗的作品。

一篇實用文的價值,就是要讓人從中可以理解實用並且正確的知識。而理解這回事要注意內容的組織架構、敘述的先後順序、以及適當地補充背景知識或是背景知識連結、沒有錯別字,以及讓人了解關鍵字。而正確的知識則需要有驗證的方式。

因為我的個性並不是很謹慎,也很容易太快下定論,雖然我儘量依照上面的原則寫作,但是還是很擔心會讓人看不懂或是閱讀困難。如果有人看到文章出現下面的情況,請在討論區留言或是從寄電子郵件到G-Mail ID: wen點cf83讓我知道。

  • 錯別字
  • 文章描述錯誤的知識、分析、或結論
  • 更好的架構組織方式。不過這部份請說明理由,我不一定會接受
  • 可以有哪些延伸主體或是更好的reference
  • 其他可以讓文章更加清晰易懂的建議

我寫部落格的動機一方面是找個主題來研究分析,更重要的是希望能讓習慣使用中文的人可以快速消化,有點概念後去讀英文資料可以快一些。「對作者仁慈就是對讀者殘忍。」,你的意見回饋將會造福之後閱讀的讀者。

尋找"Hello World\n"

很多人應該知道學C語言第一個程式

Hello.c
#include <stdio.h>

int main()
{
    printf("Hello world.\n");

    return 0;
}

不知道有沒有會問,那個"Hello world\n"放在什麼地方?我承認我用了很多年,到最近才開始去想這個問題。我想再加碼討論不同情況的"Hello world\n"執行檔會放在什麼地方?接下來我們一個一個討論吧

共用Makefile

Makefile
CFLAGS=-Wall -Werror -g
TARGET=hello
OBJS=$(patsubst %, %.o, $(TARGET))

all: $(TARGET)


%.o: %.c

    $(CC) -o $(patsubst %.o, %, $@) $^

clean:

    rm *.o *~ $(TARGET) -f

看不懂語法?唔,我也想不出來為什麼當初這樣寫,應該是剛學patsubst所以到處都想用兩下。不過只有幾行,花個時間估狗一下吧?

版本一

Hello.c
#include <stdio.h>

int main()
{
    printf("Hello world.\n");

    return 0;
}

要知道"Hello world.\n"放在那邊,可以反組譯一下,組合語言下沒有秘密。

000000000040052d <main>:
  40052d:       55                      push   %rbp
  40052e:       48 89 e5                mov    %rsp,%rbp
  400531:       bf d4 05 40 00          mov    $0x4005d4,%edi
  400536:       e8 d5 fe ff ff          callq  400410 <puts@plt>
  40053b:       b8 00 00 00 00          mov    $0x0,%eax
  400540:       5d                      pop    %rbp
  400541:       c3                      retq   
  400542:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400549:       00 00 00 
  40054c:       0f 1f 40 00             nopl   0x0(%rax)

我們可以看到沒有printf而是出現puts,目前只能猜測在沒有format string的情況下gcc會把printf換成puts,原因可能和效能有關係。

這邊關於System V X86_64的表格中我們可以看到,參數傳遞使用的暫存器依序為rdi, rsi, rdx, rcx, r8, r9

所以我們可以看到0x4005d4會被傳進puts,那麼0x4005d4在那邊呢?我們可以從symbol table中推測應該在.rodata section。

$ objdump -t hello
...
00000000004005d0 l    d  .rodata    0000000000000000              .rodata
00000000004005e4 l    d  .eh_frame_hdr  0000000000000000              .eh_frame_hdr
...

接下來我們去看.rodata裏面的內容,果然找到"Hello world.\n"

$ objdump -s -j .rodata hello

hello:     file format elf64-x86-64

Contents of section .rodata:
 4005d0 01000200 48656c6c 6f20776f 726c6400  ....Hello world.

當然這樣是不太夠,再加碼:

$ objdump -h hello | grep ro -n2
32- 13 .fini         00000009  00000000004005c4  00000000004005c4  000005c4  2**2
33-                  CONTENTS, ALLOC, LOAD, READONLY, CODE
34: 14 .rodata       00000010  00000000004005d0  00000000004005d0  000005d0  2**2
35-                  CONTENTS, ALLOC, LOAD, READONLY, DATA
36- 15 .eh_frame_hdr 00000034  00000000004005e0  00000000004005e0  000005e0  2**2

這邊顯示的資料說.rodata是唯讀的。也就是說有人想寫這塊記憶體就會GG。幸運的是這個statement似乎很難去改字串資料。


版本二

Hello.c
#include <stdio.h>

int main()
{
    char *str = "Hello world.\n";
    printf("%s", str);

    return 0;
}

一樣需要先來反組譯一下,組合語言下沒有秘密。直接挑重點。

$ objdump -d hello
...
  400535:       48 c7 45 f8 e4 05 40    movq   $0x4005e4,-0x8(%rbp)
  40053c:       00 
  40053d:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  400541:       48 89 c6                mov    %rax,%rsi
  400544:       bf f2 05 40 00          mov    $0x4005f2,%edi
  400549:       b8 00 00 00 00          mov    $0x0,%eax
  40054e:       e8 bd fe ff ff          callq  400410 <printf@plt>

可以看到這次使用了printf,而且傳了兩個參數,第一個是0x4005f2,第二個是0x4005e4。這邊要注意的是,%rsi這邊感覺有點脫褲子放屁,不知道為什麼不直接movq $0x4005e4, %rsi。不管怎樣,如第一版方式看看這兩個位址在那個section。

$ objdump -t hello
...
00000000004005e0 l    d  .rodata    0000000000000000              .rodata
00000000004005f8 l    d  .eh_frame_hdr  0000000000000000              .eh_frame_hdr
...

看起來又是.rodata,所以我們再看rodata裏面放什麼東西。

$ objdump -s -j .rodata hello

hello:     file format elf64-x86-64

Contents of section .rodata:
 4005e0 01000200 48656c6c 6f20776f 726c640a  ....Hello world.
 4005f0 00257300                             .%s.            

所以你可以看到,真正的行為是把"%s"Hello World\n這兩個字串的位址傳給printf

前面說.rodata是read only。那麼我們故意改兩下看看。

改看看放在.rodata的值
#include <stdio.h>

int main()
{
    char *str = "Hello world\n";
    printf("%s", str);
    *str = 'Q';

  return 0;
}

結果就會出現組裝工最好的朋友:Segmentation fault (core dumped)

$ ./hello 
Hello world
Segmentation fault (core dumped)

版本三

Hello3.c
#include <stdio.h>

int main()
{
    char str[] = "Hello world.\n";
    printf("%s", str);

    return 0;
}

這個可以看到是一個有初始值的陣列,而在函數內的變數會放在stack。所以我這樣測,下面結果又臭又長,用力找可以看到(精確的來說,湊到)"Hello world."的字串。

$ objdump -s -j .text hello

hello:     file format elf64-x86-64

Contents of section .text:
 4004b0 31ed4989 d15e4889 e24883e4 f0505449  1.I..^H..H...PTI
 4004c0 c7c07006 400048c7 c1000640 0048c7c7  ..p.@.H....@.H..
 4004d0 9d054000 e8b7ffff fff4660f 1f440000  ..@.......f..D..
 4004e0 b84f1060 0055482d 48106000 4883f80e  .O.`.UH-H.`.H...
 4004f0 4889e577 025dc3b8 00000000 4885c074  H..w.]......H..t
 400500 f45dbf48 106000ff e00f1f80 00000000  .].H.`..........
 400510 b8481060 0055482d 48106000 48c1f803  .H.`.UH-H.`.H...
 400520 4889e548 89c248c1 ea3f4801 d048d1f8  H..H..H..?H..H..
 400530 75025dc3 ba000000 004885d2 74f45d48  u.]......H..t.]H
 400540 89c6bf48 106000ff e20f1f80 00000000  ...H.`..........
 400550 803df10a 20000075 11554889 e5e87eff  .=.. ..u.UH...~.
 400560 ffff5dc6 05de0a20 0001f3c3 0f1f4000  ..].... ......@.
 400570 48833da8 08200000 741eb800 00000048  H.=.. ..t......H
 400580 85c07414 55bf200e 60004889 e5ffd05d  ..t.U. .`.H....]
 400590 e97bffff ff0f1f00 e973ffff ff554889  .{.......s...UH.
 4005a0 e54883ec 2064488b 04252800 00004889  .H.. dH..%(...H.
 4005b0 45f831c0 48b84865 6c6c6f20 776f4889  E.1.H.Hello woH.
 4005c0 45e0c745 e8726c64 2e66c745 ec0a0048  E..E.rld.f.E...H
 4005d0 8d45e048 89c6bf84 064000b8 00000000  .E.H.....@......
 4005e0 e89bfeff ffb80000 0000488b 55f86448  ..........H.U.dH
 4005f0 33142528 00000074 05e872fe ffffc9c3  3.%(...t..r.....
 400600 41574189 ff415649 89f64155 4989d541  AWA..AVI..AUI..A
 400610 544c8d25 f8072000 55488d2d f8072000  TL.%.. .UH.-.. .
 400620 534c29e5 31db48c1 fd034883 ec08e80d  SL).1.H...H.....
 400630 feffff48 85ed741e 0f1f8400 00000000  ...H..t.........
 400640 4c89ea4c 89f64489 ff41ff14 dc4883c3  L..L..D..A...H..
 400650 014839eb 75ea4883 c4085b5d 415c415d  .H9.u.H...[]A\A]
 400660 415e415f c366662e 0f1f8400 00000000  A^A_.ff.........
 400670 f3c3                                 .. 

當然這樣證據不夠,還是反組譯一下好了。一樣挑重點。

$ objdump -d hello
...
  4005b4:   48 b8 48 65 6c 6c 6f    movabs $0x6f77206f6c6c6548,%rax
  4005bb:   20 77 6f 
  4005be:   48 89 45 e0             mov    %rax,-0x20(%rbp)
  4005c2:   c7 45 e8 72 6c 64 2e    movl   $0x2e646c72,-0x18(%rbp)
  4005c9:   66 c7 45 ec 0a 00       movw   $0xa,-0x14(%rbp)
  4005cf:   48 8d 45 e0             lea    -0x20(%rbp),%rax
  4005d3:   48 89 c6                mov    %rax,%rsi
  4005d6:   bf 84 06 40 00          mov    $0x400684,%edi
  4005db:   b8 00 00 00 00          mov    $0x0,%eax
  4005e0:   e8 9b fe ff ff          callq  400480 <printf@plt>
...

先來猜測這一段在做啥:

  • 呼叫printf
  • 所以要帶參數,也就是"%s"和str

str的部份可以看到,其實存在stack中
首先是movabs $0x6f77206f6c6c6548,%rax

請對照ASCII 表
因為x86用little endian,所以請從右到左來看operand 0x6f77206f6c6c6548

  • 0x48H
  • 0x65e
  • 0x6cl
  • 0x6cl
  • 0x6fo
  • 0x20
  • 0x77w
  • 0x77o

接下來是4005c2: c7 45 e8 72 6c 64 2e movl $0x2e646c72,-0x18(%rbp)
一樣,對照表格可以看到

  • 0x72r
  • 0x6cl
  • 0x64d
  • 0x2e.

最後是movw $0xa,-0x14(%rbp)

  • 0xa\n

把資料存到stack後,再把stack address傳給printf,相對動作是

  4005cf:       48 8d 45 e0             lea    -0x20(%rbp),%rax
  4005d3:       48 89 c6                mov    %rax,%rsi

"%s"存在.rodata,從objdump -t hello可以看到.rodata位址是0x400680,內容是

$ objdump -s -j .rodata hello

hello:     file format elf64-x86-64

Contents of section .rodata:
 400680 01000200 257300                      ....%s. 

也就是說"%s"放在0x400684,因此直接把該位址當作參數傳給printf

  4005d6:       bf 84 06 40 00          mov    $0x400684,%edi

最後做個總結,這個版本的"Hello world.\n"程式本身"hardcode"到stack內。更簡單的說,"Hello world.\n"放在.text裏面。


版本四

Hello3.c
#include <stdio.h>

int main()
{
    static char str[] = "Hello world.\n";
    printf("%s", str);

    return 0;
}

一樣反組譯一下。直接挑重點。

$ objdump -d hello
...
  400531:       be 40 10 60 00          mov    $0x601040,%esi
  400536:       bf d4 05 40 00          mov    $0x4005d4,%edi
  40053b:       b8 00 00 00 00          mov    $0x0,%eax
  400540:       e8 cb fe ff ff          callq  400410 <printf@plt>
...

0x6010400x4005d4在那邊呢?我們可以看一下section資訊,可以看到這兩個分別落在.data.rodata

$ objdump -t hello
...
00000000004005d0 l    d  .rodata    0000000000000000              .rodata
0000000000601030 l    d  .data  0000000000000000              .data

馬上來看.data.rodata的內容

$ objdump -s -j .rodata hello

hello:     file format elf64-x86-64

Contents of section .rodata:
 4005d0 01000200 257300                      ....%s.         

$ objdump -s -j .data hello

hello:     file format elf64-x86-64

Contents of section .data:
 601030 00000000 00000000 00000000 00000000  ................
 601040 48656c6c 6f20776f 726c642e 0a00      Hello world...  

結論

依照程式的寫法,你的資料會放在不同的section。沒弄好就會把程式搞爛,有興趣的朋友可以自己設計其他實驗看看。

補充 2015-01-29

感謝Scott大大的補充。從上面的反組譯中可以看到在呼叫printf之前都會去把%eax設成零。Scott大大提醒以後才知道這樣設定是有原因的。說明如下

首先printf是一個有趣的函數,有沒有人想過為什麼他的參數的數量可以變動?我先承認我這兩年才去了解,基本上這東西叫作va_arg,細節就不談了,有興趣man va_arg就好了,那天想起來或是有人敲碗再解釋。

回到前面,因為printf是非固定參數數量,而ABI中有又有規範va_arg時候該如何傳遞。在X86的ABI規範3.5.7中提到,在傳遞這樣參數的時候,需要把要傳遞的浮點型態變數數量放在eax暫存器中。而這次範例中的printf恰巧都沒有浮點型態變數,所以將eax設成0。Scott大大沒說我還以為單純是歸零的動作orz。

參考資料

rtenv的linker script解釋

rtenv是成功大學資訊工程系同學寫出來給CM3的小型作業系統,之前使用rtenv寫作業的時候曾經trace變數trace到C code裏面沒有,但是卻在linker script找到。可是那時候看的感覺就是一堆符號,所以就沒繼續追下去。這也是我想要了解linker script的起點。看完liner script語法後,自然要回來看一下是否可以了解他的描述,先看完整語法。

main.ld
ENTRY(main)
MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
    .text :
    {
        KEEP(*(.isr_vector))
         *(.text)
         *(.text.*)
        *(.rodata)
        *(.rodata*)
        _smodule = .;
        *(.module)
        _emodule = .;
        _sprogram = .;
        *(.program)
        _eprogram = .;
        _sromdev = .;
        *(.rom.*)
        _eromdev = .;
        _sidata = .;
    } >FLASH

    /* Initialized data will initially be loaded in FLASH at the end of the .text section. */
    .data : AT (_sidata)
    {
        _sdata = .;
        *(.data)        /* Initialized data */
        *(.data*)
        _edata = .;
    } >RAM

    .bss : {
        _sbss = .;
        *(.bss)         /* Zero-filled run time allocate data memory */
        _ebss = .;
    } >RAM

    _estack = ORIGIN(RAM) + LENGTH(RAM);
}

很長令人害怕嗎?先從大方向拆解

  • 程式起始點是main
  • 使用MEMORY指令設了兩個region,分別為FLASHRAM
  • 輸出object檔案有三個section,分別是.text, .data , .bss

然後我們再往下看MEMORYSETCIONS命令的描述:

MEMORY

MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

可以看到我們有兩個region

  • FLASH
    • 唯讀、可執行
    • 起始位址0x00000000
    • 長度128k
  • RAM
    • 可讀寫和執行
    • 起始位址0x20000000
    • 長度20k

口說無憑,可以看一下CM3的Memory map,確認一下0x00000000是不是寫程式的區段,而0x20000000是不是RAM的區段(好吧是SRAM)

SECTION

剛才講過有三個section,我們一個一個分別討論:

.text

.text :
{
  KEEP(*(.isr_vector))
  *(.text)
  *(.text.*)
  *(.rodata)
  *(.rodata*)
  _smodule = .;
  *(.module)
  _emodule = .;
  _sprogram = .;
  *(.program)
  _eprogram = .;
  _sromdev = .;
  *(.rom.*)
  _eromdev = .;
  _sidata = .;
} >FLASH
  • 這個section要放在FLASH的region
  • 所有輸入object檔案的8個section會放入這個輸出object檔案section,分別為
    • .isr_vector,這個section不可以被garbage collect回收
    • .text
    • 所有.text.開頭的section
    • .rodata
    • 所有.rodata開頭的section
    • .module
    • .program
    • 所有.rom.開頭的section
  • 這個section有7個symbol,分別是
    • _smodule
      • .module起始位址
    • _emodule
      • .module結束位址
    • _sprogram
      • .program起始位址
    • _eprogram
      • .program結束位址
    • _sromdev
      • 所有.rom.開頭的section集合的起始位址
    • _eromdev
      • 所有.rom.開頭的section集合的結束位址
    • _sidata
      • .data section起始位址

這些symbol是有意義的,你需要查詢程式原始碼看他們在做三小。相信我,這值得一看。

.data

/* Initialized data will initially be loaded in FLASH at the end of the .text section. */
.data : AT (_sidata)
{
  _sdata = .;
  *(.data)        /* Initialized data */
  *(.data*)
  _edata = .;
} >RAM

這部份表示

  • .data的LMA (載入記憶體位址)是_sidata,就是.text結束的地方。另外這邊你要自己搬,有興趣請查原始碼。
  • .data要放在RAM的region
  • 所有輸入object檔案的2個section會放入這個輸出object檔案section,分別為
    • .data
    • 所有.data開頭的section
  • 這個section有2個symbol,分別是
    • _sdata
      • .data的起始位址
    • _edata
      • .data的結束位址

這些symbol是有意義的,你需要查詢程式原始碼看他們在做三小。相信我,這值得一看。

.bss

.bss : {
    _sbss = .;
    *(.bss)         /* Zero-filled run time allocate data memory */
    _ebss = .;
} >RAM
  • 所有輸入object檔案的.bss會放入這個輸出object檔案section
  • .bss要放在RAM的region
  • 這個section有2個symbol,分別是
    • _sbss
      • .bss的起始位址
    • _ebss
      • .bss的結束位址

這些symbol是有意義的,你需要查詢程式原始碼看他們在做三小。相信我,這值得一看。

stack

_estack = ORIGIN(RAM) + LENGTH(RAM);

有印象程式使用的stack是由記憶體最後面往前面長的嘛?沒印象?那就估狗linux, stack, text的圖片就可以看到了。這邊也是同樣的概念,所以他的_estack symbol位址會是RAM的開頭位址加上RAM的size。

這些symbol是有意義的,你需要查詢程式原始碼看他們在做三小。相信我,這值得一看。

GNU LD 手冊略讀 (3): Chapter 3.7 ~ Chapter 3.11

上一篇
回總目錄

本篇目錄


MEMORY命令

預設的linker會將所有的memory space視為可以分配的。然而現實生活這個假設不一定成立,例如你寫資料到ROM的記憶體就保證GG。所以linker script提供了MEMORY命令讓你畫地盤,告訴linker那塊地盤有什麼樣的特性。該命令會描述

  • 你給這塊記憶體取的名稱,也就是說前面一直講的region
  • 起始位址
  • 上面位址後面的記憶體大小
  • 這塊記憶體有什麼限制

命令語法如下:

MEMORY
{
    name [(attr)] : ORIGIN = origin, LENGTH = len
    ...
}

每個欄位說明如下

  • name
    • 你給這塊記憶體取的名稱,也就是說前面一直講的region(以下以region稱呼)。這個名稱不可以和同個linker script中以下的名稱相同:
      • symbol名稱
      • section名稱
      • 檔案名稱
    • 每塊region都要給個名字,這些名字可以給他取alias,這部份請參考REGION_ALIAS命令
  • attr
    • optional
    • 告訴linker這塊記憶體有什麼值得注意的地方,一個region可以有多個屬性,列出如下
      • R: Read only
      • W: 可讀寫
      • X: executable
      • A: 可allocate
      • IL: Initialized section (三小?)
      • !: 將該符號後面所有的屬性inverse
    • 如果一個section符合上面的條件,就可以放在這個region中
  • ORIGIN
    • 一個expression,表示該region的起始位址
  • LENGTH
    • region 大小,單位為byte

下面的範例可以看到

  • 有兩個region
  • region rom的資訊:
    • 唯讀、可執行
    • 起始位址為0
    • 長度為256k
  • region ram的資訊:
    • 非唯讀、不可執行
    • 起始位址為0x40000000
    • 長度為4M
    • 使用了縮寫,縮寫規則不想翻,請自己看這邊
MEMORY
{
    rom (rx)  : ORIGIN = 0, LENGTH = 256K
    ram (!rx) : org = 0x40000000, l = 4M
}

region和section的合體部份前面有提過了。如果沒有指定region的話,linker會從目前region挑一個給你用。除此之外,你的section空間region塞不下的話linker會幫你偵測出來。

另外ORIGIN和LENGTH可以當作查詢region的資訊,範例如下

_fstack = ORIGIN(ram) + LENGTH(ram) - 4;

PHDRS命令

資源回收上一篇講的東西。基本上我不知道elf是三小,所以很有可能這部份有錯誤,請自行斟酌!

PHDR 是ELF的program header縮寫,又稱為segment(以後以segment稱之)。當ELF loader載入ELF執行檔的時候,會看這些segment決定要如何把讀入的檔案放在記憶體中,這部份和ABI有關係,按下不表,等我那天心情好再來看ELF和ABI。你可以透過objdump -p觀察program header。

一般來說,linker預設都幫你弄好elf相關的segment。但是如果你因故需要自幹的話,就可以用PHDRS命令,一旦使用了這個命令,linker預設的相關segment設定將被取消。另外這個命令只對elf格式輸出有意義,非elf格式輸出這部份的指令一律失效。

PHDRS命令格式如下:

PHDRS
{
    name type [ FILEHDR ] [ PHDRS ] [ AT ( address ) ]
            [ FLAGS ( flags ) ] ;
}
  • name
    • 配合section命令使用,語法可以看這邊
    • segment名稱因為存放在另外的name space,所以不用擔心和symbol, 檔案, section衝突。
  • type
    • 規範為
      • PT_NULL (對應值: 0)
        • 沒使用的segment
      • PT_LOAD (對應值: 1)
        • 該segment應從檔案中載入
      • PT_DYNAMIC (對應值: 2)
        • 存放dynamic link的資訊
      • PT_INTERP (對應值: 3)
        • 指定program interpretor 路徑
        • readelf -l ls可以看到該INTERP segment的資料是是/lib64/ld-linux-x86-64.so.2,這邊似乎有些好玩的線索,一樣等到想起來再來看看。
      • PT_NOTE (對應值: 4)
        • man elf說這個是存放輔助資料
      • PT_SHLIB (對應值: 5)
        • 保留未使用
      • PT_PHDR (對應值: 6)
        • program header存放的segment
      • expression
        • 除了以上自訂的數字,應該是保留給使用自行使用...吧?
    • 每個type後面都可以加上FILEHDRPHDRS,其中
      • FILEHDR:表示該segment應該內含ELF file header
      • PHDRS:表示該segment應該內含ELF program header
  • AT
    • 指定load 位址。和section的AT相同
  • FLAGS(數字)
    • 數字是ELF的p_flagsman elf可以查到p_flags定義,數值我猜要去看程式碼或是ELF規格了。
      • PF_X: executable segment
      • PF_W: write segment
      • PF_R: read segment

單個segment通常map到一個section,linker依照順序處理header給之後的loader使用。另外要注意的是如果你在某個section指定了:phdr後,之後的section就算沒指令,都會放在該segment。如果之後的section有:phdr設成:NONE的話,linker才不會把之後的section放到任何segment。

如果有需要,你可以指定不同的segment都要有某個section的內容,使用方式就是在section命令中用多個:phdr。範例如下:

.interp : { *(.interp) } :text :interp

手冊上面提供了一個比較完整的範例。望文生義應該不難理解,所以就不解釋了。

PHDRS
{
    headers PT_PHDR PHDRS ;
    interp PT_INTERP ;
    text PT_LOAD FILEHDR PHDRS ;
    data PT_LOAD ;
    dynamic PT_DYNAMIC ;
}

SECTIONS
{
    . = SIZEOF_HEADERS;
    .interp : { *(.interp) } :text :interp
    .text : { *(.text) } :text
    .rodata : { *(.rodata) } /* defaults to :text */
    ...
    . = . + 0x1000; /* move to a new page in memory */
    .data : { *(.data) } :data
    .dynamic : { *(.dynamic) } :data :dynamic
    ...
}

VERSION命令

ELF檔案格式支援動態link的時候指定shared library版本。這項功能需要linker配合,VERSION命令就是來描述版本資訊。

語法如下

VERSION [extern "lang"] { version-script-commands }

其中 extern "lang" 的lang有支援

  • C
  • C++
  • Java

至於version-script-commands,手冊上面說和Sun(已被併購)在Solaris 2.5上的linker語法相同,估狗查version-script-commands沒查到語法,只能從手冊提供的範例來看。如果有人知道語法link請跟我說。手冊上面說這是一個樹狀結構,基本單位為一個version node。你可以在version node中設定

  • version node名稱
  • version node和相依性
  • 設定哪些symbol出現在該version node
  • 在該version node中指定global symbol變成local。如此一來,這些symbols就不會被shared library以外看到。

從手冊範例可以推測version node格式如下

name {
    [global:]
                symbol1;
        ...
    [local:]
                symbol_a;
            ...
} [depend_name];

好啦,有這樣的概念後我們來看手冊範例

VERS_1.1 {
    global:
            foo1;
    local:
            old*;
            original*;
            new*;
};

VERS_1.2 {
    foo2;
} VERS_1.1;

VERS_2.0 {
            bar1; bar2;
    extern "C++" {
                    ns::*;
                    "f(int, double)";
    };
} VERS_1.2;

OK,開始解釋:

  • 有三個Version node,名稱為VERS_1.1, VERS_1.2, VERS_2.0
  • VERS_1.1沒有相依性,VERS_1.2相依於VERS_1.1, VERS_2.0相依於VERS_1.2
  • VERS_1.1
    • symbol foo1VERS_1.1有關
    • old開頭、orignal開頭和new開頭的symbol都不會被外面看到
  • VERS_1.2
    • symbol foo2VERS_1.2有關
  • VERS_2.0
    • bar1bar2VERS_2.0有關

看完些描述後,可以問啊沒有指定和version node相關的symbol怎麼辦?手冊說會分配給library的base version(好吧我不知道base version是三小。)。如果你要將沒指定version node的symbol全部設成和某個version node有關的話,請在該version node中加上以下的描述:

  • global: *;

一般來說這個描述加再最後的version node才有意義,否則在前面的version node中把所有的symbol都被設定完畢的話,那接下來的version node就沒有辦法設定symbol關聯性的。

手冊中指出version node名稱是給人看的,對於linker在乎的只有他們的關係。所以你要故意取成讓人看不懂的名稱也可以滴。

如果你要指定所有的版本都使用同樣的symbol設定,那麼寫一份就好。重點是這份描述不用寫version node名稱,範例如下。

{ global: foo; bar; local: *; };

至於在程式碼中指定版本的方式,你需要使用GNU extention語法,例如加入下面的咒與描述到你的程式碼中。
語法如下:

__asm__(.symver name, name2@version_node_name);
  • .symver:你應該用的指令
  • name:你程式用到的symbol
  • name2@version_node_name:實際上你真正用的symbol以及對應的version node

範例如下:

__asm__(".symver original_foo,foo@VERS_1.1");

你也可以分別指定自己程式的symbol對應到不同版本的symbol,範例如下:

__asm__(".symver original_foo,foo@");
__asm__(".symver old_foo,foo@VERS_1.1");
__asm__(".symver old_foo1,foo@VERS_1.2");
__asm__(".symver new_foo,foo@@VERS_2.0");
  • foo@表示未指定版號的symbol就用該symbol
  • foo@@VERS_2.0@@表示預設使用該設定

.symver詳細的語法說明可以看這邊

下面這段是囈言囈語,因為我在描述一個我不知道什麼、以及不知道我在描述什麼的東西,請當作夢話跳過!
當你的程式要使用shared library的symbol的時候,你的程式應該要知道要用哪個版本的symbol以及這些symbol是在哪個version node宣告(怎麼做?我寫程式還要管shared library symbol版本,看linker script?不合理)。所以runtime的時候dynamic loader可以幫你搞定resolve symbol的事情。

跳過解釋需要version 的原因,想知道的可以看原文,看懂順便跟我說。

Demangled names的注意事項懶得看,一併跳過。


Linker script 中使用的expression

Linker script 的 expression有幾點特性

  • expression和C語言相同
  • 型態都是整數
  • 變數size相同,target和host為32-bit的話size就是32-bit,否則就為64-bit(為啥?那麼可不可以8, 16-bit?)。
  • expression中允許設定和讀取symbol的值

接下來我們來討論linker 中Expression可以使用的內建功能


常數

設定常數規則如下

  • 8進位
    • 0開頭
    • o結尾, O結尾
  • 16進位
    • 0x開頭, 0X開頭:
    • h結尾, H結尾
  • 10進位
    • d結尾, D結尾
  • 不屬於上面的數字表示為10進位
  • K
    • 1024
  • M
    • 1024 * 1024
  • KM不能跟下面的描述混用
    • o結尾, O結尾
    • h結尾, H結尾
    • d結尾, D結尾

範例:

_fourk_1 = 4K;
_fourk_2 = 4096;
_fourk_3 = 0x1000;
_fourk_4 = 10000o;

Symbolic 常數

指令:

  • CONSTANT(name)
    • 合法的name如下,可以望文生義所以就不解釋
      • MAXPAGESIZE
      • COMMONPAGESIZE

範例:指定.text section要和最大的page size對齊。

.text ALIGN (CONSTANT (MAXPAGESIZE)) : { *(.text) }

Symbol命名規則

沒有被"引用的情況下:

  • 允許的開頭字元
    • 大小寫英文字母
    • _
    • .
  • 名稱中間允許字元
    • 大小寫英文字母
    • 數字
    • _
    • .
    • -
  • 不可以和linker script keyword相同名稱

如果你symbol名稱要用奇怪的字元或是和keyword相同的話,請將symbol名稱的開始結尾加上"符號,範例如下:

"SECTION" = 9;
"with a space" = "also with a space" + 10;

由於symbol名稱可以有非英文字母和數字,所以不建議中間有空白字元。舉例來說,A-B是一個符號,但是A - B就是一個expression操作,表示symbol A減去symbol B


孤兒 Section

孤兒 Section 指的是在輸入object檔案中的section,而這些section linker script裏面並沒有描述該怎麼處理。遇到這種狀況,linker還是會把這些section放到輸出object檔案中,規則為:

  • 放在輸出object檔案相同性質section的後面,如程式碼或是資料、是否要load到記憶體等。如果是ELF格式的話,ELF flag也是性質比較的一部份。
  • 如果放不進去,就塞在檔案後面

如果這些孤兒section符合C語言identifier規範(通常不是以.開頭),linker會幫忙加入兩個symbol:__start_SECNAME__stop_SECNAME表示該section的起始和結束位址。而SECNAME就是該孤兒section名稱。


Location Counter

前面有提過.這個符號是location counter。而location counter本身的涵意就是目前輸出位置。而.
可以出現在SECTIONS命令中的任何expression。

除了使用.來代表目前輸出位置外,你也可以直接更改.的數值,這樣做就是更動目前輸出位置。不過要注意的是不要用做減法運算,這樣代表把目前位置往前移動,往前移動表示接下來寫入的東西就很有可能蓋掉前面重疊部份的資料。另外你可以直接把.的值加上你要的數量,那麼下一個symbol或是section就會和目前位置有一段保留空間可以使用。我們看下面的範例:

SECTIONS
{
    output :
    {
        file1(.text)
        . = . + 1000;
        file2(.text)
        . += 1000;
        file3(.text)
    } = 0x12345678;
}

這個範例我們可以看到

  • 輸出object檔案有一個section,該section名稱為output
  • output section裏面存放了
    • file1.text section
    • file2.text section
    • file3.text section
  • file1.text section和file2.text section中間相距1000
  • file2.text section和file3.text section中間也相距1000
  • 未始用到的空間請填入0x12345678,想要劇情回顧的請看這邊

.雖然是location counter,然而在不同的區塊使用會有不同的意義。
先看一個例子

SECTIONS
{
  . = 0x100
  .text: {
    *(.text)
  . = 0x200
  }
  . = 0x500
  .data: {
    *(.data)
    . += 0x600
  }
}

我們可以看到.出現的地方有四個地方,在.text.data以內有各有一個,不在.text.data以內有兩個。也就是說一種是在section描述(就是.text.data)之內,另外就是section描述之外。

  • .放在section描述裏面的話它的location是從section開頭開始算
  • .放在section描述外面的話它的location是從0

有了這樣的概念後,我們再回去看這範例在講三小?

  • 輸出object檔案有兩個section,分別為.text.data
  • 所有輸入object檔案中的.text section請放到.text
  • 所有輸入object檔案中的.data section請放到.data
  • .text section 起始點為0x100
  • 由於.被設成0x200,以致於輸入object檔案的.text存放超過0x100 + 0x200的空間都有可能被後面的資料複寫掉。
  • .text section 結束後,請保留0x500的空間
  • .text section 最後0x500的位置為.data section的起始點
  • 最後一個輸入object檔案的.data section放入輸出object的.data section後,再從section中目前位置保留0x600的空間。

手冊中有特別提到在section描述外面使用.需要特別注意的地方,它舉的例子如下:

SECTIONS
{
  start_of_text = . ;
  .text: { *(.text) }
  end_of_text = . ;
  
  start_of_data = . ;
  .data: { *(.data) }
    end_of_data = . ;
}

這個範例在.datatext前後都加了一個symbol,數值為當時location counter的位置。看起來一切安好,然而如果輸入object檔案中有section不是.data也不是.text,例如放"Hello world\n"的.rodata (參考資料),linker還是要把這些section放到輸出object檔案中。你覺得他會放在邊呢?手冊上說linker script中的symbol會被視為接在前一個section 後面,所以最後就會變成這樣:

SECTIONS
{
  start_of_text = . ;
  .text: { *(.text) }
  end_of_text = . ;
  
  start_of_data = . ;
  .rodata: { *(.rodata) }
  .data: { *(.data) }
    end_of_data = . ;
}

如此一來,如果你以為start_of_data就是.data開始位址,在你的程式中拿來做事,保證GG。因為start_of_data現在變成.rodata的起始位址了。

要確保start_of_data一定在.data section前面的話,正確的做法是在start_of_data = . ;前面加上. = .強迫更新location counter,列出完整script如下:

SECTIONS
{
  start_of_text = . ;
  .text: { *(.text) }
  end_of_text = . ;
  
  . = . ;
  start_of_data = . ;
  .data: { *(.data) }
    end_of_data = . ;
}

Operators

和C相容,請自己看,反正沒多少英文。


計算結果

原來標題是「Evaluation」直接翻成評估,估算都很詭異。自作主張就是計算結果,反正這個章節就是在講這回事。

linker很懶惰,所以不到要用的時候就不會去算expression的結果。以下是他的計算順序

  • 第一個section的起始位址,memory region size等一開始就需要取得的expression值
  • 當開始linker後才可以確定的expression,例如某個section內的symbol需要前面section處理完畢才能取得目前section的位置,然後才能開始計算裏面symbol相關的expression
  • section size也要等該section link完畢才可能計算
  • .相關的expression同要等該section link完畢才可能計算

由於有這樣的先後關係,如果你的script沒寫好可能就會遇到時間順序不同造成需要的expression裏面的element還沒計算完畢,然後你的linker就會噴錯誤出來。

手冊的範例看不懂,不解釋了。


Expression 計算結果和absolute/relative address的關係

** 這邊我不是很確定我有理解正確,請自行斟酌。另外看完後感覺上relative/absolute symbol和relative/absolute address是相同的東西,但是手冊上又沒有明講。所以我這邊語法會有點混亂。 **

這邊要先定義兩個名詞才能理解這個section在講三小。列出如下

  • relative symbol/address
  • absolute symbol/address

這兩個東西都是在講輸出object檔案的SECTIONS命令中的symbol或address。而這些symbol或 address可能宣告在section的裏面或外面。知道這樣的前提後,我們可以開始定義:

  • relative symbol/address
    • 這個symbol或address的值代表的是section到該symbol的offset
  • absolute symbol/address
    • 這個symbol或address的值和section無關,而是寫死的

知道的這樣定義後,我們可以再問,然後呢?

然後有相對特性的在relocate的時候只要更動section數值就好,而寫死的就沒有辦法動手腳。所以, relative symbol可以relocate而absolute symbol不行。

還不是很瞭長怎麼樣嘛?先看手冊的的範例好了:

SECTIONS
{
  . = 0x100;
  __executable_start = 0x100;
  .data :
  {
    . = 0x10;
    __data_start = 0x10;
    *(.data)
  }
  ...
}

我們可以看到

  • 輸出object檔案的起始位址和__executable_start為0x100,這是一個absolute address/symbol
  • .data的真正資料開始位置距離.data位置0x10,__data_start也是相同。這兩個是relative address/symbol

好啦,知道這兩個關係後,我們再回來討論計算symbol值的expression。由於linker script 命令處理回來的值有些是relative有些是absolution,所以在寫script的時候要注意。手冊描述的地方目前看不懂,懶得搞懂。不論如何,手冊提供了linker處理expresssion時對於absolute 和 relative的行為準則。

  • 計算結果為absolute
    • unary操作(如~0x11)absolute位置的address結果為absolute address
    • binary操作(如A + B),兩個operand都是absolute address的結果為absolute address
    • binary操作(如A + B),兩個operand都是數字的話的結果為absolute
    • binary操作(如A + B),兩個operand中一個是absolute address,另一個為數字的結果為absolute address
  • 計算結果為relative,假設在同一個section下
    • unary操作(如~0x11) relative位置的address結果為relative address
    • binary操作(如A + B),兩個operand都是relative address的結果為relative address
    • binary操作(如A + B),一個operand是relative address另外一個是數字的結果為relative address
  • 計算結果需要轉換後變成absolute address的情況
    • binary操作(如A + B),兩個operand都是relative address,但是是不同的section,需要先轉成absolut address再操作,的結果為absolute address
    • binary操作(如A + B),兩個operand中一個是relative address,另外一個是absolute address,需要把relative address先轉成absolut address再操作,的結果為absolute address

sub-expression(就是express裏面合法的express如a+b-c,可以拆成a+b,他的結果再跟c相加,而a+b就是一個sub-expression),的處理absolute/relative規範如下:

  • 操作處理數字結果為數字
  • 比較(|| &&)的結果也是數字
  • binary操作包含邏輯操作(如A + B),兩個operand都是relative address的結果為數字
  • binary操作包含邏輯操作(如A + B),兩個operand都是absolute address的結果為數字
  • 不是以上的操作,兩個operand都是relative address的結果為relative address
  • 不是以上的操作,一個operand是relative address另外一個是數字的結果為relative address
  • 不是以上的操作,有absolute address的操作結果為absolute

如果有需要,你可以使用ABSOLUTE()命令強迫section裏面的symbol值為absolutio,範例如下。

SECTIONS
{
    .data : { *(.data) _edata = ABSOLUTE(.); }
}

_edata沒用ABSOLUTE()命令的話會是一個relative symbol,因為加了ABSOLUTE()命令所以linker把他視為absolution symbol。


內建函數

  • ABSOLUTE(expr)
    • 把expr內的結果視為absolute的值,通常會在section內使用,用了以後這個結果將會無法relocate
  • ADDR(section)
    • 取得section名稱的VMA

前面兩個命令可以用下面範例說明

SECTIONS 
{ 
    ...
  .output1 :
  {
    start_of_output_1 = ABSOLUTE(.);
    ...
  }
  .output :
  {
    symbol_1 = ADDR(.output1);
      symbol_2 = start_of_output_1;
    ...
  }
  ... 
}

這邊我們可以看到: start_of_output_1symbol_1, 和symbol_2的值理論上是相同的。但是性質上symbol_1是relative,而其他兩個symbol是absolute。

  • ALIGN(align)
  • ALIGN(exp,align) 先講ALIGN(exp,align),這個命令是計算expr後,回傳align位址後面第一個符合alignment的位址。而ALIGN(align)可以視為ALIGN(., align),也就是說這個命令會回傳.的後面符合alignment的位址。手冊提供的範例如下,因為很容易望文生義,就不解釋了:
SECTIONS 
{ 
    ...
    .data ALIGN(0x2000): 
  {
        *(.data)
        variable = ALIGN(0x8000);
    }
    ... 
}
  • ALIGNOF(section名稱) 取得section後面符號alignment的位置。要注意的是section要已經被分配出來,否則linker會噴錯誤給你看。範例一樣容易望文生義,不解釋。
SECTIONS
{ 
    ...
  .output
  {
    LONG (ALIGNOF (.output))
    ...
  }
    ... 
}
  • BLOCK(exp)
    ALIGN()相同。是舊版的linker使用的命令。

  • DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)

  • DATA_SEGMENT_END(exp)

  • DATA_SEGMENT_RELRO_END(offset, exp)
    看不懂,不想弄懂。跳過。

  • DEFINED(symbol)
    如果symbol已經被收進symbol table就回傳1,否則回傳0。手冊示範如果沒定義該symbol就自己生一個如下。

SECTIONS 
{ 
    ...
  .text : 
  {
    begin = DEFINED(begin) ? begin : . ;
    ...
  }
  ...
}
  • LENGTH(region)
    回傳你在MEMORY命令中設定的region size

  • LOADADDR(section名稱)
    回傳section的名稱的LMA位址

  • LOG2CEIL(exp)
    取exp的log,不知道用在啥子地方。

  • MAX(exp1, exp2)
    回傳exp1和exp2比較大的數值

  • MIN(exp1, exp2)
    回傳exp1和exp2比較小的數值

  • NEXT(exp)
    回傳exp計算結果的數值記憶體之後的可使用的空間。如果沒有使用MEMORY命令設定不連續的空間,這指令效果和ALIGN命令相同。

  • ORIGIN(region名稱)
    回傳你在MEMORY命令設定的region的起始位址

  • SEGMENT_START(segment名稱, default)
    回傳segment的起始位置。還記得ELF program header的segment?我不知道和這個是不是相同。default除非有透過ld -T參數更動,否則就是預設值。手冊沒有寫預設值是多少。但是從ld --verbose看到的使用範例是用在指定程式碼開始執行的地方。有沒有覺得0x400000很眼熟呢?不熟?那算了。

  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
  . = SEGMENT_START("ldata-segment", .);
  • SIZEOF(section名稱) 回傳section的size,如果該section還沒被分配,linker就吐錯誤給你看。 下面的例子中的symbol_1symbol_2的值是相同的。這蠻容易理解,我列出來主要是要讓大家多看例子。.start, .end的用法我看過好幾次。
SECTIONS
{ 
  ...
  .output 
  {
  .start = . ;
  ...
  .end = . ;
  }
  symbol_1 = .end - .start ;
  symbol_2 = SIZEOF(.output);
... 
}
  • SIZEOF_HEADERS
  • sizeof_headers 取得輸出object檔案的header size。如果你使用ELF格式,又有自行加programer header的話,ld會噴錯誤。原因是ld預期的是ELF規範的program header,因此放不下新增的program header。所以你有多的program header的話,請不要用這個指令。

Implicit Linker Scripts

linker吃的檔案處理順序如下
1 object檔,開始link
2 不是object檔,就當linker script吃進去
3 不是object檔案也不是linker script檔案,噴錯誤然後 GG

所以,Implicit Linker Script指的是項目2吃進來的script。linker會把這個當作目前linker script的補強,而不是取代。另外由於吃進來的script順序不同,可能會出現先讀入並link 三個object檔案後,才讀到Implicit Linker Script,所以這個Implicit Linker Script無法對已經link處理。


待釐清項目

  • dynamic symbol (不知道是三小)
  • warning symbol (不知道是三小)
  • constructor symbol (不知道是三小)
  • 3.6.6看不懂,跳過。
  • dynamic loader
  • Initialize section
  • PT_INTERP和/lib64/ld-linux-x86-64.so.2的關係
  • DATA_SEGMENT_ALIGN(maxpagesize, commonpagesize)和他的朋友們

ld -M


參考資料

GNU LD 手冊略讀 (2): Chapter 3.6 SETCIONS

上一篇
下一篇
回總目錄

本篇目錄


SETCIONS命令

其實一開始是為了看懂這個命令才會想看linker script的。如果接觸過很小型的Embedded OS就會發現很多都是自幹linker script;而這些scripts主要的描述命令就是SETCIONS命令。

好了,廢話少說,進入主題。SETCIONS命令命令的功用是

  • 告訴linker怎麼把輸入object檔案中的SETCIONS命令對應到輸出object檔案中的sections
  • 告訴loader object檔案中的sections要放到記憶體那些地方

SECTIONS命令長這樣子:

SECTIONS
{
    sections-command
    sections-command
    ...
}

望文生義地猜測可以這樣理解:
輸出object有一些大方向的規範,並且分為不同的section,每個section有他自己的規範。

sections-command可以分為下面幾種功能

要注意的事,如果你自幹的linker script沒有描述輸出object檔案的setcion的話,linker會

  • 讀輸入object檔案section時,如果該section第一次出現,就在輸出object檔案中加入同樣名稱的section,直到處理完所有的輸入object檔案
  • 第一個吃到的輸入object檔案section將當作位址0的起始點

輸出object檔案的section描述


輸出object檔案的section描述格式
section [address] [(type)] :
    [AT(lma)]
    [ALIGN(section_align) | ALIGN_WITH_INPUT]
    [SUBALIGN(subsection_align)]
    [constraint]
    {
        output-section-command
        output-section-command
        ...
    } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

其中output-section-command的功能有

  • 設定symbol的值
  • 描述輸入object檔案中的section要怎麼放到輸出object檔案的setcion
  • 輸出object檔案的setcion的資料存放格式如alignment等
  • 其他

這邊很多術語需要先搞清楚,先列出來,希望之後可以看到解答

  • type
  • region
  • AT(lma)
  • lma_region

輸出object檔案的section 命名


  • 必須符合你要輸出object檔案binary format規定。

輸出object檔案section 命令: address欄位


address是section的一個optional欄位,使用的記憶體空間為VMA。如果沒有指定的話,linker會依下面的方式設定輸出object檔案section 的VMA。該VMA會遵循section 的alignment規範。

  • 有設定region的話就從region內剩餘空間開始位址
  • 有使用MEMORY命令定義硬體記憶區塊的話,從定義的區塊中挑第一個符合SECTION的區塊。再將address設成該區塊內剩餘空間開始位址
  • 以上皆非的情況下,位址設成locale counter

address欄位因為可以使用exression所以可能有下面的陷阱

  • .text . : { *(.text) }
  • .text : { *(.text) } 這兩個差一個.,意義就差很多。沒有.那個,表示沒有設定address,所以就是設成locale counter,並且linker會保證alignment。而有.的就表示hardcode成locale counter,所以有可能會有alignment的問題。

另外一點要注意的設定後locale counter也會跟著改變。

輸入object檔案的section描述


這部份可以說是整個output-section-command的重點,目的是告訴linker讀取輸入object檔案後,怎麼把這些檔案裏面的section複製到輸出object檔案裏面適當地section。

輸入object檔案的section描述可以被分為下面幾個部份

輸入object檔案的section 基礎概念


格式為檔案(section1 section2 ...),檔案支援萬用字元

所以常看到的*(.text)的意思是:所有輸入object檔案裏面的.text section。

指定多個section的方式有兩種

  • *(.sec1 .sec2):如果輸入object有兩個檔案的話,輸出object檔案裏面section會變成

  • *(.sec1) *(.sec2): 如果輸入object有兩個檔案的話,輸出object檔案裏面section會變成

你也可以根據flag區分object檔案的section,範例如下

SECTIONS {
    .text : { INPUT_SECTION_FLAGS (SHF_MERGE & SHF_STRINGS) *(.text) }
    .text2 :  { INPUT_SECTION_FLAGS (!SHF_WRITE) *(.text) }
}

望文生義可以看到上面的規範就是

  • 所有輸入object檔案的.textsection flag有SHF_MERGE 和 SHF_STRINGS的,請放在輸出object檔案的.text section
  • 所有輸入object檔案的.textsection flag沒有SHF_WRITE的,請放在輸出object檔案的.text2 section

你如果對於範例中的flag有興趣,可以看這邊, 這邊,還有這邊。我目前還不想看就是了。

另外指定輸入object檔案部份,除了指定單獨的輸入object檔案,還可以指定archieve (如libwen.a, libc.a)裏面的object檔案,用法如下
archive:file,隨便猜一個範例libc.a:fprintf.o

輸入object檔案的section 語法的萬用字元


支援
*:任何長度的任何字元
?:單一任何字元
[]:單一字元有效的範圍如[a-z]指小寫英文字母
\:接下來的字元不是萬用字元,如\*

由於linker複製section的方式是多個條件滿足的話,選第一個條件滿足就處理,所以配合萬用字元可能會產生意想不到的錯誤,範例如下

.data : { *(.data) }
.data1 : { data.o(.data) }

由於複製section的方式是第一個條件滿足就處理,所以會造成data.o的.data section放字輸出object檔案的.data section而不是.data1 section。手冊提供了建議處理方式,有興趣的可以參考

輸入object檔案的COMMOM section


  • 這邊有提到common symbol存在的原因。手冊中更進一步的提到在輸出object檔案時的命令大概是這樣:
.bss { *(.bss) *(COMMON) }

也就是說,最後沒特別狀況,就把輸入object 檔案的COMMON section放在.bss section。

KEEP指令


  • KEEP(要保留的section):因為linker有garbage collection,如果要保證section不會被回收,可以用該指令。

輸入object檔案放到輸出object檔案範例


SECTIONS {
    outputa 0x10000 :
    {
        all.o
        foo.o (.input1)
    }
    outputb :
    {
        foo.o (.input2)
        foo1.o (.input1)
    }
    outputc :
    {
        *(.input1)
        *(.input2)
    }
}

以圖示就是

輸出object檔案內指定固定資料長度

  • 長度單位命令(expression)
    • 長度單位命令
      • BYTE:1 byte
    • SHORT:2 bytes
    • LONG:4 bytes
    • QUAD:8 byte

以下的命令將會佔 5 bytes,第一個byte後面4個bytes將用來存放addr (如果我英文沒看錯的話,原文是store the byte 1 followed by the four byte value of the symbol addr':)。
BYTE(1)
LONG(addr)`

關於64-bit的目前沒心情看,跳過。

至於endian的部份,如果輸出的object檔案有規範,則依該規範存放,否則則遵守第一個讀入的輸入object檔案。

  • FILL(expression):section內沒使用的空間將被填入expression計算後的數字。同樣效果的命令是[=fillexp],忘記這是啥嗎?我也忘了,所以回去找了一下

3.6.6看不懂,跳過。

輸出object檔案捨棄的section

為什麼要丟掉?原因是在設定輸出section的script有提到特定的section,但是link完畢後發現所有輸入object檔案都沒有該section的symbol。最後就是把這些section丟掉。

另外一個情況是輸入object檔案有/DISCARD/既然就說要丟了就恭敬不如從命了。

輸出object檔案section其他屬性

還記得前面的格式嘛?再複習一下:

輸出object檔案的section描述格式
section [address] [(type)] :
    [AT(lma)]
    [ALIGN(section_align) | ALIGN_WITH_INPUT]
    [SUBALIGN(subsection_align)]
    [constraint]
    {
        output-section-command
        output-section-command
        ...
    } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

前面篇幅已經說明了sectionaddress,以及output-section-command等語法和命令,我們接著要介紹其他部份如下


輸出object檔案 Section Type

有支援

  • NOLOAD
    • 當程式執行的時候不載入到記憶體(想像ROM或是NOR FLASH)
  • DSECT
  • COPY
  • INFO
  • OVERLAY
    • 上面四個是為了往前相容保留的type,基本上很少用了。用途都相同,指定該區段不可以分配記憶體。不過我本身不懂什麼情況下不要分配記憶體就是了。

基本上type繼承輸入object中的type,不過你要硬上就是在輸出object檔案描述,範例如下。該範例顯示ROM 區段起始位址為0,並且在該section執行程式不要載入到記憶體。

SECTIONS {
    ROM 0 (NOLOAD) : { ... }
    ...
}

輸出object檔案 Section LMA

前情回顧

設定輸出object檔案的VMA是在address欄位中指定。請比對section描述格式address

LMA就是section描述格式AT(lma)AT>lma_region這兩個部份了。這兩個指令是optional的。他們的差別是:

  • AT(lma)中間的lma是透過expression算出來的lma位址
  • AT>lma_region是指定MEMORY裏面描述的region

如果你的section沒有指定LMA的話,linker會使用下面的規則決定LMA

  • address欄位中指定VMA,則LMA = VMA
  • section為allocatable,則LMA = VMA
  • 有設定region的情況在滿足下面的條件下,把VMA和LMA的差距會被設成該region裏面最後一個section中VMA和LMA的差距。
    • section滿足region條件(三小條件?)
    • 該region已經有最少一個section
  • 沒有設定region的情況在找不到相容的region,linker會指令預設包含所有memory space的region,從裏面挑一個section,把VMA和LMA的差距會被設成該region裏面最後一個section中VMA和LMA的差距。 (三小?為什麼要這樣做?)
  • 找不到合適的region放section的話,就閉著眼睛把LMA = VMA吧。

來點範例,這是一個嵌入式系統,假設所有的資料都放在唯讀記憶體中。那麼會發生什麼事呢?那就是你的i++就GG了,所以要把變數部份還有其他需要寫入的部份放在RAM中,所以這個script顯示了

  • VMA的0x1000的位址放程式碼
  • VMA的0x2000放有初始化的全域變數,而這些初始值從那邊搬到記憶體呢來呢?就是LMA描述的東西,望文生義可以知道是接在.text之後的資料。
  • VMA的0x3000就是放未初始化的全域變數
SECTIONS
{
        .text 0x1000 : { *(.text) _etext = . ; }
        .mdata 0x2000 :
            AT ( ADDR (.text) + SIZEOF (.text) )
            { _data = . ; *(.data); _edata = . ;  }
        .bss 0x3000 :
            { _bstart = . ;  *(.bss) *(COMMON) ; _bend = . ;}
}

當然事情沒那麼簡單,這邊只有講layout。在沒有OS幫你搞定的時候什麼事都要自己來,所以你還要自己把有初始化的全域變數一個一個搬到RAM裏面如下。請仔細比對變數和script的symbol。另外如果有興趣看CMSIS(Cortex Microcontroller Software Interface Standard)的source code也可以看到類似的行為。

extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext;
char *dst = &_data;

/* ROM has data at end of text; copy it.  */
while (dst < &_edata)
    *dst++ = *src++;

/* Zero bss.  */
for (dst = &_bstart; dst< &_bend; dst++)
    *dst = 0;

強制輸出object檔案的 Alignment

請使用ALIGN,或是使用ALIGN_WITH_INPUT將讀入的object檔案中的section設定成你要的alignment。


強制輸入object檔案的 Alignment

請使用SUBALIGN 去指定輸入object檔案單一個section的alignment。


輸出object檔案 Section 限制

  • ONLY_IF_RO
    • 當輸入object檔案符合條件的section為唯讀才產生你要的輸出object檔案section
  • ONLY_IF_RW
    • 當輸入object檔案符合條件的section為可讀寫才產生你要的輸出object檔案section

輸出object檔案 Section Region

使用>MEMORY_指令_宣告的region

範例,把.text放ROM section,該section位址是在硬體rom記憶體區塊。

MEMORY { rom : ORIGIN = 0x1000, LENGTH = 0x1000 }
SECTIONS { ROM : { *(.text) } >rom }

輸出object檔案 Section Phdr

PHDR 是ELF的program header縮寫,又稱為segment。當ELF loader載入ELF執行檔的時候,會看這些segment決定要如何把讀入的檔案放在記憶體中,這部份和ABI有關係,按下不表,等我那天心情好再來看ELF和ABI。

section描述格式phdr的用法是

  • 宣告一個phdr
  • 指令特定的section屬於該phdr

範例如下

PHDRS { text PT_LOAD ; }
SECTIONS { .text : { *(.text) } :text }

指定輸出object檔案 Section 填空的資料

前面FILL講到指令填空的資料。而section描述格式=fillexp也有相同效果,範例如下

SECTIONS { .text : { *(.text) } =0x90909090 }

OVERLAY命令

Overlay是一種在記憶體小於執行檔案時的技巧。其基本概念就是

  • 把程式切成不同模組
  • 載入單個模組到記憶體並執行,當程式行為Z要另外一個模組的話,就釋放目前模組,再載入新的模組到記憶體並執行。

對應到linker script就會格式這樣

OVERLAY命令格式
OVERLAY [start] : [NOCROSSREFS] [AT ( ldaddr )]
{
    secname1
    {
        output-section-command
        output-section-command
    ...
    } [:phdr...] [=fill]
    secname2
    {
        output-section-command
        output-section-command
        ...
    } [:phdr...] [=fill]
...
} [>region] [:phdr...] [=fill]

OVERLAY命令中除了OVERLAYsection 名稱以外其他都是optional。另外要注意的是OVERLAY命令不允許region和address的描述。而在OVERLAY最後面的資料固定為OVERLAY起始位址 + 最大section的size

由於OVERLAY就是動態切換並執行不同section,所以在VMA的位址會固定。這表示所有的section的VMA會相同。為了方便,linker會把所有OVERLAY中的section串接成連續的空間。

OVERLAY用法如下

  • linker script設定OVERLAY
  • 程式語言視情況需要切換時「人肉」搬移OVERLAY裏面的section到記憶體

「人肉」搬移表示我們需要

  • section 起始位址
  • section 結束位址

這部份linker會自動幫我們加入symbol,規則如下,很容易望文生義所以就不解釋了。

  • __load_start_section_名稱
  • __load_stop_section_名稱

那麼現在看一下手冊上面的範例

OVERLAY 0x1000 : AT (0x4000)
{
    .text0 { o1/*.o(.text) }
    .text1 { o2/*.o(.text) }
}

還記得addressAT命令嗎?一個是指定VMA另外一個是指定LMA。所以上面的設定白話文就是

  • 我要一個overlay,從0x4000載入到0x1000的記憶體內
  • 這個overlay有.text0.text1兩個section
  • .text0裏面放的是o1目錄下所有object檔案中的.text
  • .text1裏面放的是o2目錄下所有object檔案中的.text

那麼人肉搬移要怎麼處理呢?手冊列出如下

extern char __load_start_text1, __load_stop_text1;
memcpy ((char *) 0x1000, &__load_start_text1,
        &__load_stop_text1 - &__load_start_text1);

可以看到,我們說要從LMA搬到VMA,LMA的位址就由symbol內容提供。

另外手冊這個section我跳過一些東西,有興趣的朋友可以去超級比一比。

上一篇
下一篇
回總目錄

GNU LD 手冊略讀 (1): Chapter 3 ~ Chapter 3.5

下一篇
回總目錄

本篇目錄


Linker script 格式概論

  • 以文字檔存放
  • 由多個command組成
  • command可能是
    • keyword + 參數
    • 設定symbol
    • ...
  • command 可以用;分開,空白會被忽略
  • 使用/* .. */註解
  • 字串直接打,如果有用到script保留的字元如.可以用"包住

從Linker script 範例開始

link script 範例
SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x8000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
}

這個抄來的範例很簡單,只有一個命令SECTIONSSECTIONS是用來描述執行的時候記憶體的規劃配置(layout)。

說明這個指令細節

  • .表示記憶體位置counter,起始值為0。結束值則由linker 計算把所有input section的資料整合到output section的長度。而.如果沒有指定明確的記憶體位址的話,就會被設定為上一個位址counter的結束位址參考示意圖: (Jim Huang) How GNU Toolchain Works投影片
  • 設定記憶體位置counter為0x10000
  • 接下來請把所有輸入object檔案的程式機械碼中({ *(.text) })存放到輸出object檔案的.textsection中。
  • 接設定記憶體位置counter為0x8000000
  • 先放有初始值的全域變數(.data)
  • 再放沒有初始值的全域變數(.bss)

另外要注意的是ld會自動幫你處理alignment的問題,所以不用擔心section之間的aligment問題。


linker script 命令格式

  • ENTRY(symbol)
    • 設定某個symbol為程式執行的第一個指令起始點,在我的預設linker script中是ENTRY(_start),然後去反組譯隨便一個C編譯出來的執行檔,找字串_start可以看到裏面又去呼叫了__libc_start_main@plt
hello_word執行檔
Disassembly of section .text:

0000000000400440 <_start>:
  400440:       31 ed                   xor    %ebp,%ebp
  400442:       49 89 d1                mov    %rdx,%r9
  400445:       5e                      pop    %rsi
  400446:       48 89 e2                mov    %rsp,%rdx
  400449:       48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  40044d:       50                      push   %rax
  40044e:       54                      push   %rsp
  40044f:       49 c7 c0 c0 05 40 00    mov    $0x4005c0,%r8
  400456:       48 c7 c1 50 05 40 00    mov    $0x400550,%rcx
  40045d:       48 c7 c7 2d 05 40 00    mov    $0x40052d,%rdi
  400464:       e8 b7 ff ff ff          callq  400420 <__libc_start_main@plt>
  400469:       f4                      hlt    
  40046a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

檔案相關命令


  • INCLUDE filename
    • 在看到這個命令的時候才去載入filename這個linker script。可以被放在不同的命令如SETCTION, MEMORY等。
  • INPUT(file1 file2 ...)
    • 指定載入的輸入object檔案,如abc.o這樣的檔案。
  • GROUP(file1 file2 ...)
    • 指定載入的輸入archieve檔案,如libabc.a這樣的檔案。
  • AS_NEEDED(file1 file2 ...)
    • INPUTGROUP使用的命令,用來告訴linker說如果object裏面的資料有被reference到才link進來,猜測應該可以減少儲存空間。範例(未測試請自行斟酌):INPUT(file1.o file2.o AS_NEEDED(file3.o file4.o))
  • OUTPUT(filename)
    • gcc -o filename 一樣
  • SEARCH_DIR(path)
    • -L path一樣
  • STARTUP(filename)
    • 和INPUT相同,唯一差別是ld保證這個檔案一定是第一個被link

Object檔案相關格式命令


  • OUTPUT_FORMAT(bfdname)
    • 指定輸出object檔案的binary 檔案格式,可以使用objdump -i列出支援的binary 檔案格式
  • OUTPUT_FORMAT(default, big, little)
    • 指定輸出object檔案預設的binary 檔案格式,big endian的binary 檔案格式以及little endian的binary 檔案格式。可以使用objdump -i列出支援的binary 檔案格式
  • TARGET(bfdname)
    • 告訴ld用那種binary 檔案格式讀取輸入object檔案要,可以使用objdump -i列出支援的binary 檔案格式

設定記憶體區塊alias命令


  • REGION_ALIAS(alias, region)
    • 設定MEMORY命令中區塊的alias,一般來說,用在不同的平台需要相同的memory layout時可以使用。舉例來說,當有3個平台,記憶體layout都是相同,那麼可以
      • 將他們平台相關的記憶體區塊MEMORY命令寫在個別的檔案如linkcmds.memory
      • 設定相同的alias
      • 在主要的linker script 使用INCLUDE載入linkcmds.memory,並且直接使用alias當作一般的區塊使用。

詳細的範例說明可以看這邊

 INCLUDE linkcmds.memory
 
 SECTIONS
   {
     .text :
       {
         *(.text)
       } > REGION_TEXT
     .rodata :
       {
         *(.rodata)
         rodata_end = .;
       } > REGION_RODATA
     .data : AT (rodata_end)
       {
         data_start = .;
         *(.data)
       } > REGION_DATA
     data_size = SIZEOF(.data);
     data_load_start = LOADADDR(.data);
     .bss :
       {
         *(.bss)
       } > REGION_BSS
   }

未分類的命令 (節錄)


  • ASSERT(exp, message)
    • 條件不成立就噴訊息並結束link
  • EXTERN(symbol1 symbol2 ...)
    • 強迫讓指定的symbol設成undefined,手冊說一般用在刻意要使用非標準的API。例如自幹printf時可以用這個命令。 (不過變成了undefine symbol怎麼link??)
  • FORCE_COMMON_ALLOCATION

    • 手冊和男人說和相容性有關,手冊上是說強迫分配空間給common symbols,即使是link relocate檔案。(common symbols不知道是什麼)
  • OUTPUT_ARCH(bfdarch)

    • 指定輸出的平台,可以透過objdump -i查詢支援平台
  • INSERT [ AFTER | BEFORE ] output_section

    • 指定在預設linker script命令被執行之前或是之後加上或加入特定的輸入section到輸出section。以下是一個範例
SECTIONS
{
    OVERLAY :
    {
        .ov1 { ov1*(.text) }
        .ov2 { ov2*(.text) }
    }
}
INSERT AFTER .text;

設定symbol的值

linker script提供設定symbol數值的方法。要注意的是,這邊的symbol可以指一個全域變數、SECTION命令中的location counter(就是.開頭的資料如.text

使用方式介紹如下:

基本運算


symbol assignment operations
symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;

關於expression是三小後面會再討論。

手冊上提供的範例

symbol assign範例
floating_point = 0;
SECTIONS
{
    .text :
    {
    *(.text)
    _etext = .;
    }
    _bdata = (. + 3) & ~ 3;
    .data : { *(.data) }
}

從這邊可以看到幾種assign

  • 設定全域變數floating_point的symbol為0
  • 設定全域變數_etext的值為輸入object檔案.text合體後的offset,個人猜測可以理解成end of text。(回顧一下.是offset counter)
  • 設定全域變數_bdata的值為輸出object檔案.text結尾的offset 的4的倍數位址。這邊透露兩個資訊
    • 個人猜測可以理解成begin of data
    • 四的倍數和alignment的問題應該有關聯。

HIDDEN命令

  • HIDDEN(要隱藏的symbol) 可以把他理解成加了static的全域變數,也就是說這個symbol只在這個處理範圍中才能摸到。

PROVIDE命令


  • PROVIDE命令(symbol = expression)
    • 簡單來說,如果你的程式已經有這個symbol(函數或變數),就用你的;否則就使用這邊提供的symbol。手冊上說是給特殊的linker使用的。想知道他提的use case可以看這邊,我是沒什麼感覺。

PROVIDE_HIDDEN命令


  • PROVIDE_HIDDEN(symbol = expression)
    • 和PROVIDE命令相同,差別是這個symbol只在這個處理範圍中才能摸到,一如HIDDEN命令。

談談source code和linker script symbol的關係


這節很有趣,解答我的一些小問題。

  • 變數如何存放在binary中?
    • 先把變數名稱放入symbol table內,換句話說symbol table會多一筆資料。這筆資料的欄位有
    • symbol的位址
    • symbol的flags
    • symbol屬於哪個SECTION
    • symbol佔的記憶體空間或是alignment規範
    • symbol的名稱
    • 典型的symbol table 資料:C語言的main()
      • 00000000004005ed g F .text 0000000000000101 main
    • 而symbol的flag有7個groups
    • Group 1:
      • l: local
      • g: global
      • u: unique global,GNU 用於ELF時的 symbol binding extenstion
      • !: 既是global也是local
    • Group 2:
      • w: weak symbol
      • <空白>: strong symbol
    • Group 3:
      • C: symbol 是一個constructor (不知道這邊constructor是指那個東西? )
      • <空白>: 一般 symbol
    • Group 4:
      • W: warning symbol (不知道是三小)
      • <空白>: 一般 symbol
    • Group 5:
      • I: 間接地reference其他的symbol
      • i: relocate 時要處理的function
      • <空白>: 一般 symbol
    • Group 6:
      • D: dynamic symbol (不知道是三小)
      • d: debug symbol
      • <空白>: 一般 symbol
    • Group 7:
      • F: 這是一個function
      • f: 這是一個檔案
      • O: 這是一個object
      • <空白>: 一般 symbol
    • 如果有初始值,順便設定初始值。
  • 程式語言的取值foo = 100 runtime發生什麼事?
    • 先去symbol table找foo存在記憶體的位址,把那個位址依照symbol table的size規則將100寫入該位址。
  • 程式語言的取值ptr = &foo runtime發生什麼事?
    • 先去symbol table找foo存在記憶體的位址,把那個位址寫到ptr在symbol table對應的記憶體。
  • symbol在symbol table中存放第一個欄位是symbol的值,而這個值是一個位址
  • 在linker script設定的symbol如foo = 100和在程式碼中轉出的symbol如foo = 100差別在那?
    • linker script的100代表的是symbol的位址,而程式碼中轉出的symbol的100代表的是foo對應記憶體存放的值。
  • 如何從C語言程式碼中摸到linker script內定義的symbol?
  • 我可以反方向從linker script摸程式碼的symbol
    • 不一定,不同的程式語言和編譯器有不同的變數和函數命名方式,也就是說你原始程式碼的symbol名稱可能不是最後存在輸出object檔案的symbol 名稱。

下一篇
回總目錄