2018年12月21日 星期五

[code] 自己移動自己 - relocation

與智者同行, 你會不同凡響; 與高人為伍, 你能登上巔峰。
最近過得很舒服, 沒有在電腦技術上磨練, 看看半年來沒任何技術文章就知道了, 會跟不上快速更新的電腦世界嗎? 沒關係, 我研究的是老技術, 不是最新的技術潮流, 區塊騙, 不是是區塊鏈, 深度學習, 這些最近很紅, 通通沒再跟。

不過最新的東西有時候我還沒跟上, 它就過時了, 好險好險。

這次來談談 relocation, 對於 relocation 的概念疑惑很久了, 一直想找個方法來驗證, 知道概念很簡單, 要怎麼實作一個小程式來驗證就困難很多, 大概是 1:50 的難度。

在看過以下 2 篇有關 u-boot relocation 的文章之後, 再複習一下「程式設計師的自我修養 chapter 7」, 我終於有了實作的方向。
  1. uboot的relocation原理详细分析
  2. [uboot] (番外篇)uboot relocation介绍
建議先閱讀以上 2 篇文章, 因為以上 2 篇文章寫的內容我不會在重複介紹。

一開始思考要在 arm 還是 x86 寫這個測試程式, 畢竟是從 u-boot 借鏡而來 (u-boot 也有 x86 版本), 想了一陣子之後, 打算從 x86 來實作這個程式, 在 arm 上有些環境實在不好設置, 用 qemu 模擬不太有真實感, 用真實的開發板, 開發環境又很麻煩, 也不一定每個人都有一樣的開發板, 沒 ice/jtag 更麻煩, 所以最後還是選了 x86, 對每個人來說都是容易接觸的環境。而且 x86 和 arm 的 relocation 實作有點不同, 還好選了 x86, 我才能理解這些不同之處, 算是意外的收穫。

本來打算使用 bare-metal 方式, 但 legcy bios 512 byte 限制真的太麻煩, 最後這個程式超過 512 很多, uefi 我還沒能完全掌握, 改用 dos .com 來實作, 不怕, 之前都做過了, 不辛苦, 這是「累積」的力量, 程式本身大概花了 3 天。

期間出動了 qemu/bochs 這 2 個模擬神器, bochs 內建除錯器幫了很大的忙, 讓我找出程式的錯誤, 學會她是很有幫助的。其實我很不想靠這些工具, 希望靠自己的「冥想」可以想出問題點, 但我實在抵擋不了「時間」的誘惑, 用這些除錯工具, 我能在短時間就找到問題, 用「冥想」的話, 可能要超過一天才能想出問題吧!

疑! 不是說用模擬器會有不保險的問題嗎? 怎麼還是用了, 在 x86 上, 可以很容易把模擬器的環境在真實 PC 上測試, 沒問題的。

reloc.cpp 學習「uboot的relocation原理详细分析」這篇文章, L40 ~ 46 就是一模一樣的程式碼。

不過和 arm 的 machine code 不同, L41 test_val 不需要做 relocation 的處理。這讓我知道原來不同的 cpu, 是有不同的 relocation 情形, 而我也知道為什麼作者要加上 function pointer 的測試。最後我自己還加上 bss section 的測試, 該篇文章沒提到這個。

一開始的觀念很模糊, 不太好掌握, 什麼位址無關的程式碼, share object 之類的, 一堆和 relocate 相關的 elf section, 把我搞的一團亂, 我把這些東西都攪和在一起了, 後來選定目標, 就類似 u-boot, 把自己 relocalte 到新位址, 這麼做, 就比 share object 單純一些, 只要看 rel.dyn section 就可以。如果是要處理類似 share object 的東西, 就要看更多的 section。

要作到 relocation 需要:
  • cpu 支援
  • 編譯器支援
  • 自己還需要寫額外的程式來處理
由以上條件看來, relocation 相當不容易處理, 除了靠自己, 還得靠別人幫忙。

x86/arm 這麼流行的 cpu 當然符合第一、二個條件。

「程式設計師的自我修養」7.3.3 在說明這個, 不清楚這觀念的朋友可以參考這章節, 也會提到 GOT 這東西, 不過我的實做參考 u-boot 作法, 不使用 GOT, 只使用 rel.dyn section。

ld 加上 -pie, 就可以製作出一個可以 relocation 的執行檔。

ld -pie -m elf_i386 -static -Treloc.ld -nostdlib -o reloc.elf cpp_init.reloc.o reloc.o dos_io.o

-pie 是 ld 選項, 用來造出可 relocaltion 的執行檔, 當然並不是只加上這個選項, 你的執行檔就有能力 relocation (有這麼簡單就好了), 而是額外造出 .rel.dyn section, 讓程式碼在 relocation 時, 可以針對這些項目做修改, 進而達到 relocation 的能力。

Relocation section '.rel.dyn' at offset 0xa28 contains 28 entries:
 Offset     Info    Type            Sym.Value  Sym. Name

在移動自己的時候, 會需要這個資訊來修正對應的值。

-fPIC 則是在 share object 時會看到, 那在移動自己的時候, 需要使用 -fPIC 來編譯嗎? 你寫的程式載入到 0x100 可以執行, 載入到 0x500 也可以執行, 這就是 PIC, 和位址無關的程式碼。

如果你對於載入到 0x100, 0x500 覺得沒有差異的話, 表示 link 的概念還不熟。

測試使用了 -fno-pic + -pie 的組合, 發現還是會輸出 .rel.dyn section, 但是會多出很多項目, 猜測不需要 -fPIC 還是可以做到移動自己, 只是需要改變的項目會比較多, 這部份待以後有時間在研究。

本來我以為需要加入 -fPIC 的選項來輸出 PIC 的組合語言, 不過卻發現預設好像就是輸出 PIC 的組合語言, 不同的 gcc 版本, 預設的 option 不同。

-fPIC 會輸出 list 3 L1 這種程式, 令人看不懂的是 ebx 值是多少, 因為若不知道 ebx 的值, 就無法看懂這個組合語言, list 3 L2 之後, ebx 會是 2c3 (2c3 就是 list 3 L2 這行程式碼本身的位址), 相當於 arm 的 PC, 很神奇吧! 「程式設計師的自我修養」有說明這段 code, 就不重複了。

在 x86-64 之後, 可以直接使用 rip, 不需要在用這麼迂迴的作法, ref: x64下PIC的新寻址方式:RIP相对寻址, 有點類似 arm 的 pc。

list 3. -fPIC
1 2bd:   66 e8 bb 06 00 00       calll  97e <__x86.get_pc_thunk.bx>
2 2c3:   66 81 c3 1d 0a 00 00    add    $0xa1d,%ebx

編譯器已經做好他該做的事情, 剩下的該程式本身自己來了, 需要做的有:
  1. 複製程式本身到新位址
  2. 修改需要 relocation 的變數/函式
  3. 如何跳到新位址
大家猜猜看, 那個最難?

沒有相關經驗的程式員, 光第一點就會難倒人。因為這需要修改 linker script, 一般程式員幾乎是不會去修改這個的, 但這只是最簡單的第一關而已。

這次的範例程式大膽的使用了 c++, 甚至使用了 c++17 標準 (因為我用了 g++8), 但其實和 c 不會差太多, 因為 c++17 的很多特性我也不會, 有些地方麻煩了一點, 但已經難不倒我了。

reloc.cpp
  1 __asm__(".code16gcc\n");
  2 #include "io.h"
  3 #include "obj.h"
  4 
  5 typedef signed char s8;
  6 typedef signed short s16;
  7 typedef signed int s32;
  8 
  9 typedef unsigned char u8;
 10 typedef unsigned short u16;
 11 typedef unsigned int u32;
 12 
 13 extern "C" void jmp_to_reloc_addr();
 14 extern "C" u32 get_pc();
 15 
 16 #define R_386_RELATIVE 0x00000008
 17 
 18 #define BOCHS_MB __asm__ __volatile__("xchg %bx, %bx");
 19 
 20 extern int _start_ctors;
 21 extern int _end_ctors;
 22 
 23 void s16_print_int(int i, int radix);
 24 void print_str(const char   *s);
 25 
 26 void s32_memcpy(u8 *dest, const u8 *src, u32 n)
 27 {
 28   for (u32 i=0; i < n ; ++i)
 29     *dest++ = *src++;
 30 }
 31 
 32 void test_func(void)
 33 {
 34   print_str("test func\r\n");
 35   u32 v = get_pc();
 36   s16_print_int(v, 16);
 37   print_str("\r\n");
 38 }
 39 
 40 static auto test_func_val = test_func;
 41 static int test_val = 10;
 42 int data1;
 43 
 44 void (*data2)(void);
 45 
 46 int rel_dyn_test()
 47 {
 48   BOCHS_MB
 49   print_str("data_1: ");
 50   s16_print_int(data1, 16);
 51   print_str("\r\n");
 52   //data1 = 5;
 53   data2 = test_func;
 54   int i;
 55   i =  test_val;
 56   print_str("test_func_val: ");
 57   s16_print_int((int)test_func_val, 16);
 58   print_str("\r\n");
 59   (*test_func_val)();
 60     //printf("test = 0x%x\n", test_func);
 61     //printf("test_func = 0x%x\n", test_func_val);
 62   test_func();
 63   return i + data1;
 64 }
 65 
 66 extern int __image_copy_start;
 67 extern int __image_copy_end;
 68 extern int __rel_dyn_start;
 69 extern int __rel_dyn_end;
 70 extern u32 __bss_start__;
 71 extern u32 __bss_end__;
 72 
 73 void init_reloc_bss(u32 reloc_offset)
 74 {
 75   u32 reloc_bss_b = (int)&__bss_start__ + reloc_offset;
 76   u32 reloc_bss_e = (int)&__bss_end__ + reloc_offset;
 77   print_str("reloc_bss_b: ");
 78   s16_print_int(reloc_bss_b, 16);
 79   print_str("\r\n");
 80   print_str("reloc_bss_e: ");
 81   s16_print_int(reloc_bss_e, 16);
 82   print_str("\r\n");
 83   for (u32 b = reloc_bss_b ; b < reloc_bss_e ; b++)
 84   {
 85     *(u8*)b = 1;
 86   }
 87 }
 88 
 89 void reloc(u32 reloc_addr)
 90 {
 91   int from = (int)&__image_copy_start;
 92   int to = (int)&__image_copy_end;
 93   int image_size = to - from;
 94   u32 reloc_off = reloc_addr - from;
 95 
 96   print_str("reloc_addr: ");
 97   s16_print_int(reloc_addr, 16);
 98   print_str("\r\n");
 99 
100   print_str("reloc_off: ");
101   s16_print_int(reloc_off, 16);
102   print_str("\r\n");
103 
104 
105   int rel_dyn_from = (int)&__rel_dyn_start;
106   int rel_dyn_to = (int)&__rel_dyn_end;
107 
108   s16_print_int(from, 16);
109   print_str("\r\n");
110   s16_print_int(to, 16);
111   print_str("\r\n");
112   s16_print_int(rel_dyn_from, 16);
113   print_str("\r\n");
114   s16_print_int(rel_dyn_to, 16);
115   print_str("\r\n");
116 
117   s32_memcpy((u8*)reloc_addr, (u8*)from, image_size);
118   init_reloc_bss(reloc_off);
119   s16_print_int(image_size, 16);
120   print_str("\r\n");
121 
122   // modify rel.dyn section
123   for (int i = rel_dyn_from ; i < rel_dyn_to ; i+=8)
124   {
125     u32 v1 = *(u32*)i;
126     u32 v2 = *(u32*)(i+4);
127     #if 0
128     print_str("v1: ");
129     s16_print_int(v1, 16);
130     print_str("\r\n");
131 
132     print_str("v2: ");
133     s16_print_int(v2, 16);
134     print_str("\r\n");
135     #endif
136     if (v2 == R_386_RELATIVE)
137     {
138       u32 mem_data = *(u32*)(v1 + reloc_off); // 0xa2c
139       #if 0
140       print_str("mem_data: ");
141       s16_print_int(mem_data, 16);
142       print_str("\r\n");
143       #endif
144 
145       *(u32*)(v1+reloc_off) = mem_data + reloc_off; // locate offset
146       mem_data = *(u32*)(v1 + reloc_off);
147       #if 0
148       print_str("after reloc mem_data: ");
149       s16_print_int(mem_data, 16);
150       print_str("\r\n");
151       #endif
152     }
153     print_str("\r\n");
154   }
155 
156   //s16_print_int(rel_dyn_to, 16);
157   //print_str("\r\n");
158   //s16_print_int(v, 16);
159   //print_str("\r\n");
160 
161 }
162 
163 extern "C" int cpp_main(void)
164 {
165   print_str("cpp_main\r\n");
166   //s16_print_int(obj_count, 10);
167   rel_dyn_test();
168   u32 v = get_pc();
169   print_str("before reloc pc: ");
170   s16_print_int(v, 16);
171   print_str("\r\n");
172 
173   reloc(0x1100);
174   jmp_to_reloc_addr();
175 
176   print_str("after reloc to %cs:0x1100\r\n");
177 
178   v = get_pc();
179   print_str("after reloc pc: ");
180   s16_print_int(v, 16);
181   print_str("\r\n");
182 
183   rel_dyn_test();
184   
185   return 0;
186 }

[移動自己]

這需要靠 linker script 幫忙, reloc.ld L7, L24 將執行檔本身的 text, rodata, data section 的開始/結束位址記錄下來, 將他們複製到新的位址 0x1100 即可, 選擇移動到 cs:0x1100 也不是胡亂選的, 這是思考之後的決定, 最主要是除錯時比較方便。

reloc.cpp L117 的 s32_memcpy 就是在做這件事情。

reloc.ld
 1 /* for cb.c */
 2 ENTRY(_start);
 3 SECTIONS
 4 {
 5 
 6     . = 0x100;
 7     __image_copy_start = .;
 8     .text :
 9     {
10         *(.text)
11         *(.gnu.linkonce.t*)
12     }
13     .rodata :
14     {
15         *(.rodata*)
16         *(.gnu.linkonce.r*)
17     }
18 
19     .data :
20     {
21         *(.data.*)
22         *(.gnu.linkonce.d*)
23     }
24     __image_copy_end = .;
25 
26  . = ALIGN(4);
27 
28 __rel_dyn_start = .;
29  .rel.dyn : {
30   *(.rel.dyn*)
31  }
32 __rel_dyn_end = .;
33 
34     /* for g++ 4.7 */
35     .init_array :
36     {
37       __start_global_ctor__ = .;
38     }
39     __end_global_ctor__ = .;
40     .ctors :
41     {
42       start_ctors = .; _start_ctors = .; __start_ctors = .;
43       *(.ctor*)
44       end_ctors = .; _end_ctors = .; __end_ctors = .;
45 /*      . = ALIGN(0x1000); */
46      }
47      /*
48     .dtors :
49     {
50       start_dtors = .; _start_dtors = .; __start_dtors = .;
51       *(.dtor*)
52       end_dtors = .; _end_dtors = .; __end_dtors = .;
53       . = ALIGN(0x1000);
54      }
55      */
56 
57 
58     .bss :
59     {
60         sbss = .; __bss_start__ = .;
61         *(.bss)
62         ebss = .; __bss_end__ = .;
63         *(COMMON)
64         *(.gnu.linkonce.b*)
65     }
66 
67     /DISCARD/ :
68     {
69         *(.comment)
70         *(.eh_frame) /* discard this, unless you are implementing runtime support for C++ exceptions. */
71     }
72 }

簡單吧!

[修正 test_func_val]

list 2 L10, 11 是 test_val, L21, 22 是 test_func_val, 和 arm 的版本不同, x86 沒有 lable 這種東西, 直接就定位到 static 變數的位址, 所以處理 relocation 的方式也不同。

需要修改的只有 test_func_val, 因為 test_func_val 的值是 23d (list 2 L29,30), 是 test_func() 的位址, relocate 之後, test_func() 會變成 123d (我把整個程式從 cs:0x100 移動到 cs:0x1100), 所以 test_func_val (a48, list 2 L29) 的值要從 23d 改成 0x123d, 但是 test_func_val 也從 a48 移動到 1a48 了, 所以真正要改的位址是 1a48, 其內容要改成 0x123d, 很複雜吧! 知道理論和寫出程式難度還真的不同, 理論的話, 剛剛提到的細節都不用管, 只要知道 test_func_val 要做 relocation 的修改即可。

再來就是我怎麼會知道要改這個呢? 總不能每次都人工反組譯看吧, 所以編譯器很義氣的幫我們產生 rel.dyn section, 這樣就知道要改的就是這個地方。

list 2 L54 .rel.dyn section 不就告訴我們 a48 這個地方要調整嗎? 就是我上述說明的那樣修改。
修改方式: https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-54839.html#chapter6-26
就是下表 Table 12-15, R_386_RELATIVE 的改法, 取 32bit 長度, 加上一個 base address。

32-bit x86: Relocation Types

The relocations that are listed in the following table are defined for 32–bit x86.
Table 12-15 32-bit x86: ELF Relocation Types

Name
Value
Field
Calculation
R_386_NONE
0
None
None
R_386_32
1
word32
S + A
R_386_PC32
2
word32
S + A - P
R_386_GOT32
3
word32
G + A
R_386_PLT32
4
word32
L + A - P
R_386_COPY
5
None
Refer to the explanation following this table.
R_386_GLOB_DAT
6
word32
S
R_386_JMP_SLOT
7
word32
S
R_386_RELATIVE
8
word32
B + A
R_386_GOTOFF
9
word32
S + A - GOT
R_386_GOTPC
10
word32
GOT + A - P
R_386_32PLT
11
word32
L + A
R_386_16
20
word16
S + A
R_386_PC16
21
word16
S + A - P
R_386_8
22
word8
S + A
R_386_PC8
23
word8
S + A - P
R_386_SIZE32
38
word32
Z + A



[bss section 的修改]
bss section 一樣被移動到 0x1100 的地方, 所以也需要修改其內容,

L72, L80 2c3+a05+54 = d1c
剛好是 data1 的位址

readelf -a reloc.elf
45: 00000d1c 4 OBJECT GLOBAL DEFAULT 14 data1
52: 00000d1c 0 NOTYPE GLOBAL DEFAULT 14 __bss_start__
57: 00000d24 0 NOTYPE GLOBAL DEFAULT 14 __bss_end__

data1 位於 bss section, 而 bss 落在 00000d1c - 00000d24, 8 byte 就是 data1, data2 佔的空間。
移動之後的 bss 會落在 1d1c ~ 1d24, 只要把這裡清成 0 即可。

[跳到 relocation 之後的位址]
這是個大問題, 也是理論派不會注意的地方, 因為這也和平台有關, arm 是這樣做, x86 又是另外的作法。我用了類似 __x86.get_pc_thunk.b 來移動到新的位址, 這是我自己想出來的作法, 不確定有沒有什麼比較正規的作法。

呼叫 jmp_to_reloc_addr() 之後會 jmp 到新的位址, 怎麼辦到的? jmp_to_reloc_addr() 結束之後, 會執行下一個位址也就是 reloc.cpp L176 print_str(), 我得讓他跳到新的 print_str() 而不是原來的那個 print_str(), 只要我可以得知這個位址, 加上 0x1100 - 0x100 就可以跳到新位址的 print_str(), jmp_to_reloc_addr() 就是在做這件事情。

list 2 的反組譯和 reloc.cpp 有點不同, 因為 reloc.cpp 我後來有再次修改, 反組譯的結果就不更新了。

list 2. objdump -m i8086 -CD reloc.elf
91 0000023d <test_func()>:
92  23d: 66 55                   push   %ebp
93  23f: 66 89 e5                mov    %esp,%ebp
94  242: 66 53                   push   %ebx
95  244: 66 83 ec 14             sub    $0x14,%esp
96  248: 66 e8 47 07 00 00       calll  995 <__x86.get_pc_thunk.bx>
97  24e: 66 81 c3 f2 08 00 00    add    $0x8f2,%ebx
98  255: 66 83 ec 0c             sub    $0xc,%esp
99  259: 67 66 8d 83 5c fe ff    lea    -0x1a4(%ebx),%eax
90  260: ff 
91  261: 66 50                   push   %eax
92  263: 66 e8 e5 04 00 00       calll  74e <print_str(char const*)>
93  269: 66 83 c4 10             add    $0x10,%esp
94  26d: 66 e8 d2 fe ff ff       calll  145 <get_pc>
95  273: 67 66 89 45 f4          mov    %eax,-0xc(%ebp)
96  278: 67 66 8b 45 f4          mov    -0xc(%ebp),%eax
97  27d: 66 83 ec 08             sub    $0x8,%esp
98  281: 66 6a 10                pushl  $0x10
99  284: 66 50                   push   %eax
90  286: 66 e8 96 06 00 00       calll  922 <s16_print_int(int, int)>

 1 000002b2 <rel_dyn_test()>:


71  2bd:   66 e8 2d 08 00 00       calll  af0 <__x86.get_pc_thunk.bx>
72  2c3:   66 81 c3 05 0a 00 00    add    $0xa05,%ebx
73  2ca:   87 db                   xchg   %bx,%bx
74  2cc:   66 83 ec 0c             sub    $0xc,%esp
75  2d0:   67 66 8d 83 3e fe ff    lea    -0x1c2(%ebx),%eax
76  2d7:   ff
77  2d8:   66 50                   push   %eax
78  2da:   66 e8 c9 05 00 00       calll  8a9 <print_str(char const*)>
79  2e0:   66 83 c4 10             add    $0x10,%esp
80  2e4:   67 66 8b 83 54 00 00    mov    0x54(%ebx),%eax // data1


 2  2b2:   66 55                   push   %ebp
 3  2b4:   66 89 e5                mov    %esp,%ebp
 4  2b7:   66 53                   push   %ebx
 5  2b9:   66 83 ec 14             sub    $0x14,%esp
 6  2bd:   66 e8 ab 06 00 00       calll  96e <__x86.get_pc_thunk.bx>
 7  2c3:   66 81 c3 45 08 00 00    add    $0x845,%ebx
 8  2ca:   87 db                   xchg   %bx,%bx
 9                                        
10                                        test_val
11  2cc:   67 66 8b 83 64 ff ff    mov    -0x9c(%ebx),%eax
12  2d3:   ff 
13  2d4:   67 66 89 45 f4          mov    %eax,-0xc(%ebp)
14  2d9:   66 83 ec 0c             sub    $0xc,%esp
15  2dd:   67 66 8d 83 7c fe ff    lea    -0x184(%ebx),%eax
16  2e4:   ff 
17  2e5:   66 50                   push   %eax
18  2e7:   66 e8 3a 04 00 00       calll  727 <print_str(char const*)>
19  2ed:   66 83 c4 10             add    $0x10,%esp
20                            
21                                        test_func_val
22  2f1:   67 66 8b 83 40 ff ff    mov    -0xc0(%ebx),%eax
23  2f8:   ff 
24  2f9:   66 83 ec 08             sub    $0x8,%esp
25  2fd:   66 6a 10                pushl  $0x10
26  300:   66 50                   push   %eax
27  302:   66 e8 f3 05 00 00       calll  8fb <s16_print_int(int, int)>
28 
29 00000a48 <test_func_val>:
30  a48:   3d 02 00                cmp    $0x2,%ax
31         ...
32 
33 
34 00000a6c <test_val>:
35  a6c:   0a 00                   or     (%bx,%si),%al
36         ...
37  
38 Disassembly of section .dynamic:
39  
40 00000a70 <_DYNAMIC>:     
41  a70:   04 00                   add    $0x0,%al
42  a72:   00 00                   add    %al,(%bx,%si)
43  a74:   20 0a                   and    %cl,(%bp,%si)
44  a76:   00 00                   add    %al,(%bx,%si)
45  a78:   f5                      cmc
46  a79:   fe                      (bad)
47 
48 descent@debian64:dos_cpp$ readelf -r reloc.elf
49 
50 Relocation section '.rel.dyn' at offset 0xb14 contains 3 entries:
51  Offset     Info    Type            Sym.Value  Sym. Name
52 0000014e  00000008 R_386_RELATIVE
53 00000154  00000008 R_386_RELATIVE
54 00000a48  00000008 R_386_RELATIVE

[relocate stack]
需要 relocate stack 嗎? u-boot 有 relocate stack 嗎? 我不知道, 但就算要做也不困難。好吧! 有點難, 和我想的不太一樣, 花了一天搞定, 嘴炮果然輕鬆多了。

本來使用 c++ function 來寫這功能, 一直有錯, 後來用了 bochs 除錯, 看了很多組合語言之後, 決定改用組合語言來寫這段 (因為 c++ 怎麼寫都寫不出來), 終於搞定。

list 3. qemu 執行結果
 1 Booting from Hard Disk...
 2 Boot failed: could not read the boot disk
 3 
 4 Booting from Floppy...
 5 Starting MS-DOS...
 6 
 7 A:\>reloc
 8 cpp_main
 9 data_1: 0
10 test_func_val: 26A
11 test func
12 2A0
13 test func
14 2A0
15 before reloc pc: 968
16 reloc_addr: 1100
17 reloc_off: 1000
18 100
19 DB4
20 E70
21 EB8
22 reloc_bss_b: 1EB8
23 reloc_bss_e: 1EC0
24 CB4
25 ---
26 yy from: 
27 yy to: 258A26AC
28 after reloc to %cs:0x1100
29 after reloc pc: 15F2
30 data_1: 1010101
31 test_func_val: 126A
32 test func
33 12A0
34 test func
35 12A0
36 
37 A:\>QEMU: Terminated





這個 relocation 的 dos 程式是用 g++ 8.2/c++17 開發的, 雖然用模擬器跑過了, 但不在真實機器上執行過一次, 就是不放心。

不過測試過程比我想的還難一點, 一開始找的 usb 隨身碟都無法正常開機, 突然想到我的 usb floppy, 好吧! 也可以, 剩下的 2 片 3.5 磁碟片, 只剩下一片是好的, 這是我最後一片可以用的 3.5 磁片了。

正常開啟 dos 時, 看到了久違的 A:\>, 懷念的感覺突然湧現, 當敲下鍵盤執行 reloc 時, 心中很緊張, 怕會執行錯誤, 好在結果是正確的, 程式正常的執行成功, 也印出預期的結果, 當然也正常的結束。

這個程式會在 cs:0x100 上執行, 然後將自己複製/移動到 cs:0x1100 並把 cs:0x100 的原本程式碼清成0, 再重新執行同一個函式。

bss/stack 也都做了 relocate 的動作, 最後在回到 dos。

dos 6.22 也能跟上 c++17 呢!

test func 在 relocate 之前印出 2A0, 在 relocate 之後印出 12A0, 同一個函式執行 2 次, 印出不同的 pc, 確認位址真的被移動到 0x1100 開始。事實上, 我做了更多的確認, 確認程式本身, bss, stack 真的都被移動到 offset 0x1000 (0x1100 - 0x100) 上, 全靠 bochs 的內建除錯器才完成。

這個技術有什麼功用呢? 說真的, 我還真不知道可以幹麻, 雖然 share object 有類似的東西, 但細節又有點不同, 也許可以當作處理 share object 的前哨站。看看能不能寫一個載入 share object 的程式, 也許下次可以挑戰看看。

u-boot 這麼做有他自己的理由, 但我實在想不出一般「正常」的程式用這招可以幹麻, 就 have fun 吧!

在完成這個程式之後, 我理解了部份 Dynamic section, 但為了不讓本篇更複雜, Dynamic section 補在最後面, 照理來說應該透過 Dynamic section 把 rel.dyn section 找出來, 而不是用 __rel_dyn_start, __rel_dyn_end 這個方式, 但這樣比較簡單, 也簡化整個程式的撰寫。

list 5 L12, 13, 14 就是用來找出 rel.dyn section, 大小是 24 byte, 每一個 entry 是 8 byte, 所以總共有 3 個 entry, rel.dyn section 位址在 0xe58, 就是 __rel_dyn_start 這個地方。

看來 relocation 還有很多相關的東西可以寫, 就留到來日吧!

list 5
 1 descent@debian64:dos_cpp$ readelf -d reloc.elf
 2 
 3 Dynamic section at offset 0xdb4 contains 15 entries:
 4   Tag        Type                         Name/Value
 5  0x00000004 (HASH)                       0xd64
 6  0x6ffffef5 (GNU_HASH)                   0xd74
 7  0x00000005 (STRTAB)                     0xd60
 8  0x00000006 (SYMTAB)                     0xd50
 9  0x0000000a (STRSZ)                      1 (bytes)
10  0x0000000b (SYMENT)                     16 (bytes)
11  0x00000015 (DEBUG)                      0x0
12  0x00000011 (REL)                        0xe58
13  0x00000012 (RELSZ)                      24 (bytes)
14  0x00000013 (RELENT)                     8 (bytes)
15  0x00000016 (TEXTREL)                    0x0
16  0x0000001e (FLAGS)                      TEXTREL
17  0x6ffffffb (FLAGS_1)                    Flags: PIE
18  0x6ffffffa (RELCOUNT)                   3
19  0x00000000 (NULL)                       0x0

寫這個程式時, 經常出動 readelf, objdump 等相關程式, 學會使用他們也是很重要的。

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。