2013年2月21日 星期四

x86 call/ret 會 push/pop 2byte or 4 byte in stack??

最近踩到這個地雷, 這是在我要將 kernel loader 改為使用 big real mode 以便載入超過 1M 的 kernel 時遇到的。在 x86 16bit 環境下使用 gas, gcc 產生的程式碼有點小差異。

16 111: e8 44 00 call 158 <init_bss_asm>

L16 這是 gas 組語產生的 machine code。

69 1ac: 66 e8 65 ff ff ff calll 117 <asm_4g_memcpy>

L69 這是 gcc c 語言產生的 machine code。多了一個 66 prefix, 改變 operand size 大小。在 16 bit 模式下, operand size 會改變成 32bit, 本來使用 call 指令後 (in 16 bit mode), %esp 是 -2, 但是加了這個 0x66 prefix %esp 會 -4, 所以要用 retl 才會讓 %esp +4 回來。L54, L71 的 ret 指令就差了一個 0x66 也是類似的道理。

結果是這樣的:
呼叫 init_bss_asm 前後, %esp 會加減 2; 呼叫 asm_4g_memcpy, %esp 卻會加減 4。
asm_4g_memcpy 是組語 function, 我在 c code 呼叫 asm_4g_memcpy, %esp 會減 4。而原本 asm_4g_memcpy 結尾寫 ret, 離開 asm_4g_memcpy 返回原 function 後, %esp 會加 2,  要寫成 retl 才會加 4。原本我是寫 ret, 結果造成 stack 在呼叫 asm_4g_memcpy 之後會出問題。

這問題花了我兩個下午才找到, 還得靠著 bochs 才能發現。除非直些看 machine code, 否則很難發現這問題。因為:

gcc -DIPC -std=c99 -fno-stack-protector -m32 -ffreestanding -fno-builtin -g -Iinclude -I../include  -S kernel_loader.c

從 kernel_loader.s 只能看到 call asm_4g_memcpy, 而不是 calll asm_4g_memcpy, 看不到 66 這個 prefix。

call 和 calll 會翻成不同的 machine code (16bit mode):
call -> e8 XX
calll -> 66 e8 XX

objdump -m i8086 -d kloaderp.bin.elf
1
2 kloaderp.bin.elf: file format elf32-i386
3
4
5 Disassembly of section .text:
6
7 00000100 <_start>:
8 100: 87 db xchg %bx,%bx
9 102: 8c c8 mov %cs,%ax
10 104: 8e d8 mov %ax,%ds
11 106: 8e c0 mov %ax,%es
12 108: 8e e0 mov %ax,%fs
13 10a: 8e e8 mov %ax,%gs
14 10c: 8e d0 mov %ax,%ss
15 10e: bc f0 ff mov $0xfff0,%sp
16 111: e8 44 00 call 158 <init_bss_asm>
17 114: e8 5d 00 call 174 <start_c>
18
19 00000117 <asm_4g_memcpy>:
20 117: 66 55 push %ebp
21 119: 66 89 e5 mov %esp,%ebp
22 11c: 66 56 push %esi
23 11e: 66 57 push %edi
24 120: 66 51 push %ecx
25 122: 67 66 8b 7d 08 mov 0x8(%ebp),%edi
26 127: 67 66 8b 75 0c mov 0xc(%ebp),%esi
27 12c: 67 66 8b 4d 10 mov 0x10(%ebp),%ecx
28 131: 66 83 f9 00 cmp $0x0,%ecx
29 135: 74 0f je 146 <asm_4g_memcpy+0x2f>
30 137: 67 8a 06 mov (%esi),%al
31 13a: 66 46 inc %esi
32 13c: 64 67 88 07 mov %al,%fs:(%edi)
33 140: 66 47 inc %edi
34 142: 66 49 dec %ecx
35 144: eb eb jmp 131 <asm_4g_memcpy+0x1a>
36 146: 67 66 8b 45 08 mov 0x8(%ebp),%eax
37 14b: 66 59 pop %ecx
38 14d: 66 5f pop %edi
39 14f: 66 5e pop %esi
40 151: 66 89 ec mov %ebp,%esp
41 154: 66 5d pop %ebp
42 156: 66 c3 retl
43
44 00000158 <init_bss_asm>:
45 158: bf c0 05 mov $0x5c0,%di
46 15b: be c0 05 mov $0x5c0,%si
47 15e: eb 0f jmp 16f <init_bss_asm+0x17>
48 160: 66 b8 00 00 00 00 mov $0x0,%eax
49 166: 89 f0 mov %si,%ax
50 168: 67 c6 00 01 movb $0x1,(%eax)
51 16c: 83 c6 01 add $0x1,%si
52 16f: 39 fe cmp %di,%si
53 171: 75 ed jne 160 <init_bss_asm+0x8>
54 173: c3 ret
55
56 00000174 <start_c>:
57 174: 66 55 push %ebp
58 176: 66 89 e5 mov %esp,%ebp
59 179: 66 83 ec 28 sub $0x28,%esp
60 17d: 67 c7 45 f6 00 00 movw $0x0,-0xa(%ebp)
61 183: 67 66 c7 45 f0 64 00 movl $0x64,-0x10(%ebp)
62 18a: 00 00
63 18c: 67 66 0f b7 45 f6 movzwl -0xa(%ebp),%eax
64 192: 67 66 c7 44 24 08 00 movl $0x200,0x8(%esp)
65 199: 02 00 00
66 19c: 67 66 89 44 24 04 mov %eax,0x4(%esp)
67 1a2: 67 66 8b 45 f0 mov -0x10(%ebp),%eax
68 1a7: 67 66 89 04 24 mov %eax,(%esp)
69 1ac: 66 e8 65 ff ff ff calll 117 <asm_4g_memcpy>
70 1b2: 66 c9 leavel
71 1b4: 66 c3 retl

測試 source code:

kloader_init.S
1 /*
2 * the kernel loader will initialize c runtime and enter x86 protected mode
3 */
4
5
6 #define BIG_REAL_MODE
7
8 .data
9
10 LABEL_STACK:
11 .space 1024, 0
12 .set top_of_stack, (. - LABEL_STACK)
13
14
15
16 .code16
17 .extern __bss_start__
18 .extern __bss_end__
19
20 .text
21 .global _start
22 _start:
23 xchg %bx, %bx
24 mov %cs, %ax
25 #mov $0xa00, %ax
26 mov %ax, %ds
27 mov %ax, %es
28 mov %ax, %fs
29 mov %ax, %gs
30
31 mov %ax, %ss
32 # setup stack
33 mov $0xfff0, %sp # why not setting 0xffff to %sp, in ms dos, 0xfff0 is ok, 0xffb will get core dump
34
35 call init_bss_asm
36 call start_c

71
72 # use big real mode copy to 4g range
73 # use %fs for segment register, %fs points to 4g memory space
74 .globl asm_4g_memcpy
75 # ex:
76 # asm_absolute_memcpy((u8*)0x100, (u8 *)IMAGE_LMA, 512*3);
77 # copy IMAGE_LMA to 0x100
78 asm_4g_memcpy:
79 pushl %ebp
80 mov %esp, %ebp
81
82 pushl %esi
83 pushl %edi
84 pushl %ecx
85
86 mov 8(%ebp), %edi /* Destination */
87 mov 12(%ebp), %esi /* Source */
88 mov 16(%ebp), %ecx /* Counter */
89 1:
90 cmp $0, %ecx /* Loop counter */
91 jz 2f
92 movb %ds:(%esi), %al
93 inc %esi
94 movb %al, %fs:(%edi)
95 inc %edi
96 dec %ecx
97 jmp 1b
98 2:
99 mov 8(%ebp), %eax
100 pop %ecx
101 pop %edi
102 pop %esi
103 mov %ebp, %esp
104 pop %ebp
105 retl
106
107 # init bss
108 init_bss_asm:
109 movw $__bss_end__, %di /* Destination */
110 movw $__bss_start__, %si /* Source */
111 # movw $0x0, %ax
112 # movw %ax, %gs
113 jmp 2f
114 1:
115 mov $0, %eax
116 movw %si, %ax
117 movb $0x1, (%eax)
118 # movb $0x1, %ds:(%eax)
119 add $1, %si
120
121 2:
122 cmpw %di, %si
123 jne 1b
124 ret

kernel_loader.c
1 __asm__(".code16gcc\n");
2
3 typedef signed char s8;
4 typedef signed short s16;
5 typedef signed int s32;
6
7 typedef unsigned char u8;
8 typedef unsigned short u16;
9 typedef unsigned int u32;
10
11 typedef unsigned long long u64;
12
13
14 void asm_4g_memcpy(u32 dest, u32 src, int n);
15 void start_c()
16 {
17 u16 buff=0;
18 u32 kernel_addr=100;
19 asm_4g_memcpy(kernel_addr, (u16)buff, 512);
20 }

完整範例:
https://github.com/descent/progs/tree/master/gas_gcc_16bit

很開心的以為搞定, 結果遇上另外的難題

而若是在組合語言 call c function, 要用 calll c_func, 不能用 call c_func, 免得 %esp 又差了 2。

ref: x86/x64 指令编码内幕(适用于 AMD/Intel)

沒有留言:

張貼留言

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

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