目的
これまでベアメタル環境では、全てアセンブリだけでプログラムを書いてきました。
しかし、今後更に多くの実験を行うにはアセンブリだけでは負担が大きくなってきます。
そこで本記事では、ベアメタル環境でC言語を使えるようにする手順をまとめます。
今回はブートローダ → 32bit プロテクトモードへ移行 → C 言語の関数呼び出しまでを実装します。
Hello from C! が出力されれば成功です。
コード
[org 0x7C00]
bits 16
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
sti
; --- INT 13h CHS 読み込み ---
mov ah, 0x02 ; BIOS: Read Sectors
mov al, 1 ; 読み込むセクタ数 (1)
mov ch, 0 ; シリンダ = 0
mov dh, 0 ; ヘッド = 0
mov cl, 2 ; セクタ = 2 (ブートローダの次)
mov dl, 0x80 ; ドライブ番号=HDD
mov bx, 0x7E00 ; ES:BX = 読み込み先
int 0x13
jc disk_error
jmp 0x0000:0x7E00 ; 読み込んだコードへジャンプ
disk_error:
hlt
jmp disk_error
times 510-($-$$) db 0
dw 0xAA55
bits 16
global start
extern kernel_main
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
mov ax, 0x0003
int 0x10
; VGAリアルモード表示
mov ax, 0xB800
mov es, ax
; GDTロード
lgdt [gdt_descriptor]
; プロテクトモード有効化
mov eax, cr0
or eax, 1
mov cr0, eax
; 32ビットコードへ移行
jmp 0x08:pm_start
; -----------------------------
gdt_start:
dq 0x0000000000000000 ; NULL
gdt_code:
dw 0xFFFF ; limit 0-15
dw 0x0000 ; base 0-15
db 0x00 ; base 16-23
db 0x9A ; code segment
db 0xCF ; flags
db 0x00 ; base 24-31
gdt_data:
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92 ; data segment
db 0xCF
db 0x00
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
; -----------------------------
[bits 32]
pm_start:
; データセグメント設定
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x9FC00
call kernel_main
.hlt_loop:
hlt
jmp .hlt_loop
void putc(char c) {
static int pos = 0;
volatile unsigned short* vram = (unsigned short*)0xB8000;
vram[pos++] = 0x0F00 | c; //属性 背景色 = 0x0 = 黒 文字色 = 0xF = 白
}
void puts(const char* s) {
while (*s) putc(*s++);
}
void kernel_main() {
puts("Hello from C!");
while(1);
}
ENTRY(start)
SECTIONS {
. = 0x7E00;
.text : {
*(.text*)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
*(COMMON)
}
}
コンパイル及び実行方法
nasm -f bin boot.asm -o boot.bin
gcc -m32 -ffreestanding -c kernel.c -o kernel.o
nasm -f elf32 test.asm -o test.o
ld -m elf_i386 -T linker.ld -o kernel.elf test.o kernel.o
objcopy -O binary kernel.elf kernel.bin
cat boot.bin kernel.bin > disk.img
結合の処理
extern kernel_mainの意味
kernel.c → kernel.o 中に kernel_mainというラベルがある前提。
ld が test.o と kernel.o を結合するので
test.asm の call kernel_main が C の関数と正しく繋がる。
linker.ldの意味
test.asm + kernel.cを 0x7E00 に配置される前提でリンクする。
test.asm の start = 0x7E00
pm_start も 0x7E00 からの相対位置
kernel_main も 0x7E00 からの相対位置
として 絶対アドレスが決まる。
実行
QEMU
qemu-system-i386 -hda disk.img
ThinkPad X280
lsblk
sudo dd if=disk.img of=/dev/sdb bs=512 count=2 conv=notrunc
kernel.cをコンパイルして得られるアセンブリを確認する
gcc -m32 -ffreestanding -S -masm=intel kernel.c -o kernel.s
.file "kernel.c"
.intel_syntax noprefix
.text
.globl putc
.type putc, @function
putc:
.LFB0:
.cfi_startproc
push ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
mov ebp, esp
.cfi_def_cfa_register 5
push ebx
sub esp, 20
.cfi_offset 3, -12
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
mov edx, DWORD PTR 8[ebp]
mov BYTE PTR -24[ebp], dl
mov DWORD PTR -8[ebp], 753664
movsx dx, BYTE PTR -24[ebp]
mov ebx, edx
or bh, 15
mov edx, DWORD PTR pos.0@GOTOFF[eax]
lea ecx, 1[edx]
mov DWORD PTR pos.0@GOTOFF[eax], ecx
mov eax, edx
lea edx, [eax+eax]
mov eax, DWORD PTR -8[ebp]
add eax, edx
mov edx, ebx
mov WORD PTR [eax], dx
nop
mov ebx, DWORD PTR -4[ebp]
leave
.cfi_restore 5
.cfi_restore 3
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size putc, .-putc
.globl puts
.type puts, @function
puts:
.LFB1:
.cfi_startproc
push ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
mov ebp, esp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
jmp .L3
.L4:
mov eax, DWORD PTR 8[ebp]
lea edx, 1[eax]
mov DWORD PTR 8[ebp], edx
movzx eax, BYTE PTR [eax]
movsx eax, al
push eax
call putc
add esp, 4
.L3:
mov eax, DWORD PTR 8[ebp]
movzx eax, BYTE PTR [eax]
test al, al
jne .L4
nop
nop
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size puts, .-puts
.section .rodata
.LC0:
.string "Hello from C!"
.text
.globl kernel_main
.type kernel_main, @function
kernel_main:
.LFB2:
.cfi_startproc
push ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
mov ebp, esp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
lea eax, .LC0@GOTOFF[eax]
push eax
call puts
add esp, 4
.L6:
jmp .L6
.cfi_endproc
.LFE2:
.size kernel_main, .-kernel_main
.local pos.0
.comm pos.0,4,4
.section .text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB3:
.cfi_startproc
mov eax, DWORD PTR [esp]
ret
.cfi_endproc
.LFE3:
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0"
.section .note.GNU-stack,"",@progbits
mov DWORD PTR -8[ebp], 753664の753664を16進数で表すと0xB8000となり、テキストモードのVRAM開始アドレスと一致する。
生成されたバイナリ(disk.img)を逆アセンブルして確認する
boot.asm
16ビットとして逆アセンブルした結果
ndisasm -b 16 -o 0x7C00 disk.img | less
00007C00 FA cli
00007C01 31C0 xor ax,ax
00007C03 8ED8 mov ds,ax
00007C05 8EC0 mov es,ax
00007C07 8ED0 mov ss,ax
00007C09 BC007C mov sp,0x7c00
00007C0C FB sti
00007C0D B402 mov ah,0x2
00007C0F B001 mov al,0x1
00007C11 B500 mov ch,0x0
00007C13 B600 mov dh,0x0
00007C15 B102 mov cl,0x2
00007C17 B280 mov dl,0x80
00007C19 BB007E mov bx,0x7e00
00007C1C CD13 int 0x13
00007C1E 7205 jc 0x7c25
00007C20 EA007E0000 jmp 0x0:0x7e00
00007C25 F4 hlt
00007C26 EBFD jmp short 0x7c25
00007C28 0000 add [bx+si],al
00007C2A 0000 add [bx+si],al
00007C2C 0000 add [bx+si],al
disk.imgの32ビットモードへ入る前
513バイト〜の領域にはtest.asmの内容が入っている
16ビットとして逆アセンブルした結果
dd if=disk.img of=sec1.bin bs=512 skip=1 count=1
ndisasm -b 16 -o 0x7E00 sec1.bin | less
00007E00 FA cli
00007E01 31C0 xor ax,ax
00007E03 8ED8 mov ds,ax
00007E05 8EC0 mov es,ax
00007E07 8ED0 mov ss,ax
00007E09 BC007C mov sp,0x7c00
00007E0C B80300 mov ax,0x3
00007E0F CD10 int 0x10
00007E11 B800B8 mov ax,0xb800
00007E14 8EC0 mov es,ax
00007E16 0F0116427E lgdt [0x7e42]
00007E1B 0F20C0 mov eax,cr0
00007E1E 6683C801 or eax,byte +0x1
00007E22 0F22C0 mov cr0,eax
00007E25 EA487E0800 jmp 0x8:0x7e48
※これより下はデータや32ビットの機械語なので16ビットとして逆アセンブリ出来ない。
00007E25 EA487E0800 jmp 0x8:0x7e48を実行すると32ビットモードへ移行する。
disk.imgのC言語呼び出し部分
jmp 0x8:0x7e48から32ビットコードの位置が分かるので、48バイト以降を切り出す。
dd if=sec1.bin of=pm.bin bs=1 skip=$((0x48)) status=none
ndisasm -b 32 pm.bin
32ビットとして逆アセンブルする。
dd if=sec1.bin of=sec1_7e9c.bin bs=1 skip=$((0x9C)) status=none
ndisasm -b 32 -o 0x7E9C sec1_7e9c.bin | less
call 0x9cがCの入り口と見える。※元々call kernel_mainと書いている。
0x9cからのコードを見てみると上に載せたkernel.sと一致している。
00000000 66B81000 mov ax,0x10
00000004 8ED8 mov ds,eax
00000006 8EC0 mov es,eax
00000008 8EE0 mov fs,eax
0000000A 8EE8 mov gs,eax
0000000C 8ED0 mov ss,eax
0000000E BC00FC0900 mov esp,0x9fc00
00000013 E884000000 call 0x9c
00000018 F4 hlt
00000019 EBFD jmp short 0x18
0000001B 55 push ebp
0000001C 89E5 mov ebp,esp
0000001E 53 push ebx
0000001F 83EC14 sub esp,byte +0x14
00000022 E893000000 call 0xba
00000027 0531010000 add eax,0x131
0000002C 8B5508 mov edx,[ebp+0x8]
0000002F 8855E8 mov [ebp-0x18],dl
00000032 C745F800800B00 mov dword [ebp-0x8],0xb8000
00000039 660FBE55E8 movsx dx,[ebp-0x18]
0000003E 89D3 mov ebx,edx
00000040 80CF0F or bh,0xf
00000043 8B900C000000 mov edx,[eax+0xc]
00000049 8D4A01 lea ecx,[edx+0x1]
0000004C 89880C000000 mov [eax+0xc],ecx
00000052 89D0 mov eax,edx
00000054 8D1400 lea edx,[eax+eax]
00000057 8B45F8 mov eax,[ebp-0x8]
0000005A 01D0 add eax,edx
0000005C 89DA mov edx,ebx
0000005E 668910 mov [eax],dx
00000061 90 nop
00000062 8B5DFC mov ebx,[ebp-0x4]
00000065 C9 leave
00000066 C3 ret
00000067 55 push ebp
00000068 89E5 mov ebp,esp
0000006A E84B000000 call 0xba
0000006F 05E9000000 add eax,0xe9
00000074 EB18 jmp short 0x8e
00000076 8B4508 mov eax,[ebp+0x8]
00000079 8D5001 lea edx,[eax+0x1]
0000007C 895508 mov [ebp+0x8],edx
0000007F 0FB600 movzx eax,byte [eax]
00000082 0FBEC0 movsx eax,al
00000085 50 push eax
00000086 E890FFFFFF call 0x1b
0000008B 83C404 add esp,byte +0x4
0000008E 8B4508 mov eax,[ebp+0x8]
00000091 0FB600 movzx eax,byte [eax]
00000094 84C0 test al,al
00000096 75DE jnz 0x76
00000098 90 nop
00000099 90 nop
0000009A C9 leave
0000009B C3 ret
0000009C 55 push ebp
0000009D 89E5 mov ebp,esp
0000009F E816000000 call 0xba
000000A4 05B4000000 add eax,0xb4
000000A9 8D8066FFFFFF lea eax,[eax-0x9a]
000000AF 50 push eax
000000B0 E8B2FFFFFF call 0x67
000000B5 83C404 add esp,byte +0x4
000000B8 EBFE jmp short 0xb8
000000BA 8B0424 mov eax,[esp]
000000BD C3 ret
000000BE 48 dec eax
000000BF 656C gs insb
000000C1 6C insb
000000C2 6F outsd
000000C3 206672 and [esi+0x72],ah
000000C6 6F outsd
000000C7 6D insd
000000C8 204321 and [ebx+0x21],al
000000CB 001400 add [eax+eax],dl
000000CE 0000 add [eax],al
000000D0 0000 add [eax],al
000000D2 0000 add [eax],al
000000D4 017A52 add [edx+0x52],edi
000000D7 0001 add [ecx],al
000000D9 7C08 jl 0xe3
000000DB 011B add [ebx],ebx
000000DD 0C04 or al,0x4
000000DF 0488 add al,0x88
000000E1 0100 add [eax],eax
000000E3 0020 add [eax],ah
000000E5 0000 add [eax],al
000000E7 001C00 add [eax+eax],bl
000000EA 0000 add [eax],al
000000EC 2F das
000000ED FF db 0xff
000000EE FF db 0xff
000000EF FF4C0000 dec dword [eax+eax+0x0]
000000F3 0000 add [eax],al
000000F5 41 inc ecx
000000F6 0E push cs
000000F7 088502420D05 or [ebp+0x50d4202],al
000000FD 44 inc esp
000000FE 830302 add dword [ebx],byte +0x2
00000101 44 inc esp
00000102 C5 db 0xc5
00000103 C3 ret
00000104 0C04 or al,0x4
00000106 0400 add al,0x0
00000108 1C00 sbb al,0x0
0000010A 0000 add [eax],al
0000010C 40 inc eax
0000010D 0000 add [eax],al
0000010F 0057FF add [edi-0x1],dl
00000112 FF db 0xff
00000113 FF3500000000 push dword [dword 0x0]
00000119 41 inc ecx
0000011A 0E push cs
0000011B 088502420D05 or [ebp+0x50d4202],al
00000121 71C5 jno 0xe8
00000123 0C04 or al,0x4
00000125 0400 add al,0x0
00000127 0018 add [eax],bl
00000129 0000 add [eax],al
0000012B 006000 add [eax+0x0],ah
0000012E 0000 add [eax],al
00000130 6C insb
00000131 FF db 0xff
00000132 FF db 0xff
00000133 FF1E call far [esi]
00000135 0000 add [eax],al
00000137 0000 add [eax],al
00000139 41 inc ecx
0000013A 0E push cs
0000013B 088502420D05 or [ebp+0x50d4202],al
00000141 0000 add [eax],al
00000143 0010 add [eax],dl
00000145 0000 add [eax],al
00000147 007C0000 add [eax+eax+0x0],bh
0000014B 006EFF add [esi-0x1],ch
0000014E FF db 0xff
0000014F FF0400 inc dword [eax+eax]
00000152 0000 add [eax],al
00000154 0000 add [eax],al
00000156 0000 add [eax],al
00000158 0000 add [eax],al
0000015A 0000 add [eax],al
0000015C 0000 add [eax],al
0000015E 0000 add [eax],al
00000160 0000 add [eax],al
00000162 0000 add [eax],al
よって各ソースがコンパイル後に正しく結合されていることが分かる。
開発環境
test@test-fujitsu:~/kaihatsu/c$ uname -a
Linux test-fujitsu 6.8.0-87-generic #88~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Oct 14 14:03:14 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
test@test-fujitsu:~/kaihatsu/c$ gcc -m32 --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
test@test-fujitsu:~/kaihatsu/c$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.38
Copyright (C) 2022 Free Software Foundation, Inc.
这个程序是自由软件;您可以遵循GNU 通用公共授权版本 3 或
(您自行选择的) 稍后版本以再次散布它。
这个程序完全没有任何担保。
test@test-fujitsu:~/kaihatsu/c$ qemu-system-i386 --version
QEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.27)
Copyright (c) 2003-2021 Fabrice Bellard and the QEMU Project developers
test@test-fujitsu:~/kaihatsu/c$


