x86 시스템 프로그래밍을 학습하기 위한 수십 개의 최소 운영체제(OS)입니다. QEMU 2.11과 실제 하드웨어의 Ubuntu 18.04 호스트에서 테스트되었습니다. 유저랜드 치트(Userland cheat)는 다음에서 확인 가능합니다: https://github.com/cirosantilli/linux-kernel-module-cheat#userland-assembly ARM baremetal 설정은 다음에서 확인 가능합니다: https://github.com/cirosantilli/linux-kernel-module-cheat#baremetal-setup
-
- China
-
- Getting started
-
- Minimal examples
-
- BIOS
-
- No BIOS
-
- Modes of operation
-
- in and out instructions
-
- Video mode
-
- Power
-
- UEFI
-
- Coreboot
-
- GRUB
-
- Multiboot
-
- Tests
-
- About
-
- Bibliography
-
- LICENSE
먼저 이 소개 글을 읽어보세요: https://stackoverflow.com/questions/22054578/how-to-run-a-program-without-an-operating-system/32483545#32483545
그런 다음 Ubuntu에서:
./configure make
최상위 레벨의 모든 .S 파일은 운영체제입니다! 이들은 해당되는 .img 파일로 컴파일됩니다.
기본 OS를 QEMU에서 실행하려면:
./run
특정 OS를 실행하려면:
./run bios_hello_world ./run bios_putc
예시는 다음에서 설명합니다:
확장자는 완벽한 탭 자동 완성(tab completion)을 위해 무시되므로, 다음은 모두 동일합니다:
./run min ./run min. ./run min.S ./run min.img
QEMU 대신 Bochs를 사용하려면:
./run bios_hello_world bochs
그런 다음 터미널에서 다음으로 시뮬레이션을 시작합니다:
c
Bochs를 종료하려면 다음 중 하나를 수행하세요:
- GUI 내부의 전원 끄기 버튼을 누르거나
- 터미널에서 Ctrl + C를 누르고
quit을 입력한 후 엔터를 누릅니다.
TODO: 이 단계를 자동화합니다.
참고 문헌(Bibliography): https://stackoverflow.com/questions/6142925/how-can-i-use-bochs-to-run-assembly-code/32871939#32871939
USB를 삽입하고, 다음 명령어로 장치(/dev/sdX)를 확인합니다:
sudo lsblk sudo fdisk -l
실행하려는 .img 파일을 선택하고:
sudo dd if=bios_hello_world.img of=/dev/sdX
그런 다음:
- 컴퓨터에 USB를 삽입합니다.
- 부팅 중, 보통 F12나 Esc와 같은 특수 하드웨어 종속 키를 누릅니다.
- USB에서 부팅하도록 선택합니다.
작업을 마쳤으면 전원 버튼을 눌러 종료하면 됩니다.
예를 들어, 제 T430에서는 다음과 같이 보입니다.
전원을 켠 후에는 부팅 메뉴로 진입하기 위해 Enter 키를 눌러야 합니다:
그런 다음, 여기서는 F12 키를 눌러 USB를 부팅 장치로 선택해야 합니다:
그렇게 하면 다음과 같이 USB를 부팅 장치로 선택할 수 있습니다:
또는, 부팅 순서를 변경하여 USB가 더 높은 우선순위를 갖도록 설정하면 매번 수동으로 선택할 필요가 없습니다. 이 경우 'Startup Interrupt Menu' 화면에서 F1 키를 누른 다음 다음 경로로 이동합니다:
GRUB에서 부팅 가능한 모든 예제를 포함하는 big.img 생성하기:
make big.img
이제 다음과 같이 실행하면:
sudo dd if=big.img of=/dev/sdX
단일 USB 플래싱으로 여러 예제를 테스트할 수 있어 훨씬 빠릅니다.
재미로 QEMU에서 big image를 시도해 볼 수도 있습니다:
qemu-system-i386 -hda big.img
또한 F12 BIOS 메뉴에서 USB가 첫 번째로 오도록 부팅 순서를 변경하는 것이 좋습니다. 이렇게 하면 매번 F12 키를 누르지 않아도 됩니다.
TODO: STAGE2를 로드하는 부트 섹터가 big image chainloader와 작동하지 않습니다. TODO 왜일까요?
Ubuntu 환경이 없다면, 첫 실행을 위해 다음과 같은 쉬운 대안이 있습니다:
sudo docker run --interactive --tty --name xbme --net=host ubuntu:18.04 bash
그리고 이후 실행은 다음과 같습니다:
sudo docker start xbme sudo docker exec --interactive --tty xbme bash sudo docker stop xbme
나중에 컨테이너를 삭제하려면:
sudo docker rm xbme
그 후 게스트 환경에서 정상적으로 진행합니다: 패키지 설치 및 빌드:
apt-get update &&
apt-get install -y git &&
git clone https://github.com/cirosantilli/x86-bare-metal-examples &&
cd x86-bare-metal-examples &&
./configure -y &&
make
GUI가 부족한 문제를 해결하기 위해, 기본 SDL 대신 QEMU의 VNC 구현을 사용할 수 있습니다. 이는 --net=host 때문에 호스트에서 보입니다.
:
./run bios_hello_world run -vnc :0
그리고 호스트에서는:
sudo apt-get install vinagre vinagre localhost:5900
프로그램을 GDB로 단계별 디버깅하려면 다음과 같이 실행합니다:
./run bios_hello_world debug
이는 프로그램이 실행하는 첫 번째 명령어에서 멈추게 됩니다. 즉, 우리의 BEGIN 매크로의 시작점입니다.
다만, 이것이 QEMU가 실제로 실행하는 가장 첫 번째 명령어는 아니라는 점에 유의해야 합니다. 실제로는 우리 프로그램보다 먼저 실행되는 BIOS 설정 코드가 있을 것입니다.
그 후에는 일반적인 사용자 영역(userland) 프로그램을 디버깅하듯이 기본적으로 작업할 수 있습니다. 특히 다음을 참고하세요:
ni는 매크로를 건너뛰고 -
ni는 매크로 내부에서 스텝합니다. 하지만 어디에 있는지 확인하려면 GDB Dashboard에서 어셈블리 코드 출력 기능을 활성화해야 합니다.
이러한 God-like한 GDB Dashboard 설정으로, 89cbe7be83f164927caebc9334bc42990e499cb1 지점에서는 다음과 같은 완벽한 프로그램 보기를 확인할 수 있습니다:
1 /* https://github.com/cirosantilli/x86-bare-metal-examples#bios-hello-world */ 2 3 #include "common.h" 4 BEGIN 5 mov $msg, %si 6 mov $0x0e, %ah 7 loop: 8 lodsb 9 or %al, %al 10 jz halt 11 int $0x10 12 jmp loop
─── 어셈블리 (Assembly) ────────────────────────────────────────────────────────────────────────────
0x00007c00 __start+0 cli
0x00007c01 __start+1 ljmp $0xc031,$0x7c06
0x00007c08 __start+8 mov %eax,%ds
0x00007c0a __start+10 mov %eax,%es
0x00007c0c __start+12 mov %eax,%fs
0x00007c0e __start+14 mov %eax,%gs
0x00007c10 __start+16 mov %eax,%ebp
0x00007c12 __start+18 mov %eax,%ss
0x00007c14 __start+20 mov %ebp,%esp
─── 레지스터 (Registers) ───────────────────────────────────────────────────────────────────────────
eax 0x0000aa55 ecx 0x00000000 edx 0x00000080 ebx 0x00000000 esp 0x00006f04 ebp 0x00000000 esi 0x00000000 edi 0x00000000 eip 0x00007c00 eflags [ IF ] cs 0x00000000 ss 0x00000000 ds 0x00000000 es 0x00000000 fs 0x00000000 gs 0x00000000
─── 스택 (Stack) ───────────────────────────────────────────────────────────────────────────────
[0] from 0x00007c00 in __start+0 at bios_hello_world.S:4 (no arguments)
─────────────────────────────────────────────────────────────────────────────────────────>>>
디버그 심볼(Debug symbols)은 먼저 ELF 파일을 링크한 다음, objcopy를 사용하여 얻을 수 있습니다.
최종 이미지를 생성하도록 합니다. 그런 다음 디버그 정보를 가진 ELF 파일을 GDB에 전달합니다: https://stackoverflow.com/questions/32955887/how-to-disassemble-16-bit-x86-boot-sector-code-in-gdb-with-x-i-pc-it-gets-tr/32960272#32960272
int 호출 건너뛰기(step over) 방법: http://stackoverflow.com/questions/24491516/how-to-step-over-interrupt-calls-when-debugging-a-bootloader-bios-with-gdb-and-q
특정 opcode에서까지 단일 스텝(Single stepping)하는 것이 때때로 도움이 될 수 있습니다: https://stackoverflow.com/questions/14031930/break-on-instruction-with-specific-opcode-in-gdb/31249378#31249378
TODO: 제어 레지스터(control registers)에서 16비트인지 32비트인지 자동으로 감지하는 기능. 현재는 16과 32라는 두 함수를 사용하여 수동으로 전환하고 있는데, 이게 별로입니다. 문제는 이들을 직접 읽을 수 없다는 것입니다: http://stackoverflow.com/a/31340294/895245 만약 cr0이 있다면, hook-stop 내부에서 if cr0 & 1 같은 코드로 쉽게 할 수 있을 텐데요.
이것들이 먼저 살펴보아야 할 것들입니다.
make -C printf run
결과: QEMU 창이 열리고 몇 가지 부팅 메시지가 출력된 후 멈춥니다(hangs).
저희 프로그램 자체는 화면에 아무것도 출력하지 않고, 단지 CPU를 정지시킵니다(halt).
이 예제는 printf로 바이트 단위로 생성되었습니다. 이보다 더 최소화할 수는 없습니다!
기본적으로 다음으로 구성되어 있습니다:
- 0번째 바이트:
hlt 명령어-
- 1바이트부터 509바이트까지: 영(zeroes), 아무거나 될 수 있음
- 510바이트와 511바이트: 필수 매직 바이트인
0xAA55, 이는 BIOS가 저희 디스크로 인식하는 데 필요합니다.
./run min
결과: QEMU 창이 열리고 몇 가지 펌웨어 메시지가 출력된 후 멈춥니다(hangs).
저희 미니 라이브러리를 사용한 동등한 예제는 다음과 같습니다:
./run template
새로운 테스트를 시작하는 빠른 템플릿으로 이 파일을 사용할 수 있습니다.
hlt을 사용하는 대신 무한 루프(infinite loop)로 들어갑니다:
./run infinite_loop
결과는 눈에 띄게 같지만, TODO: 실제 하드웨어에서는 전력을 더 많이 소모할 가능성이 높습니다?
이 중요한 파일은 저희 어셈블리 메모리 레이아웃을 결정하므로, 이 파일의 주석들을 읽고 숙지하는 데 시간을 할애하십시오.
hello world 출력하기
펌웨어 메시지 이후에 실행할 명령어는 다음과 같습니다:
./run bios_hello_world
cd c_hello_world ./run
하지만 다음 제한 사항과 어려움들을 염두에 두어야 합니다:
단일 스테이지(single stage)이므로 여전히 코드 및 데이터 512바이트로 제한됩니다! TODO: BIOS 디스크 로드를 사용하면 쉽게 해결될 것이라 생각합니다. 풀 리퀘스트를 보내주세요 :-) 우리가 적응할 수 있는 전체 예제는 여기입니다: http://3zanders.co.uk/2017/10/18/writing-a-bootloader3
GCC의 -m을 사용하면 "진짜" 16비트 코드를 생성하지 않고, 대신 0x66 및 0x67 접두사를 가진 32비트 코드가 생성됩니다: https://wiki.osdev.org/X86-64_Instruction_Encoding#Legacy_Prefixes -
초기 상태 설정과 링커 스크립트를 구성하는 것이 어셈블리(assembly)를 사용할 때보다 훨씬 어렵고 오류가 발생하기 쉽습니다.
따라서 대부분의 애플리케이션에서는 이러한 모든 문제를 극복하는 Multiboot을 사용하는 것이 좋습니다.
생성된 C 코드를 디스어셈블(disassemble)하려면 다음 명령어를 시도해 보세요:
objdump -D -m i8086 main.elf
하지만 여전히 32비트 참조를 포함하고 있다는 점에 유의해야 합니다. 예를 들어:
00007c17 <main>: 7c17: 66 55 push %ebp 7c19: 66 89 e5 mov %esp,%ebp 7c1c: 66 83 ec 10 sub $0x10,%esp
이는 해당 명령어들이 접두사 0x66에 의해 수정되어 마치 32비트처럼 작동하기 때문입니다.
명시적인 링커 스크립트를 사용하지 않고 hello world를 출력하는 방법:
make -C no-linker-script run
소스 코드:
기본 호스트 ld 스크립트를 사용하며, -T로 명시적으로 설정된 스크립트는 사용하지 않습니다. 또한 다음을 사용합니다:
각 어셈블리 파일 내부의 -tText
.org
경고를 피하기 위해 _start가 반드시 존재해야 합니다. 기본 링커 스크립트가 이를 예상하기 때문입니다.
이것은 임시방편(hack)이며, 빠르고 간편한 테스트에는 편리할 수 있지만 절대 사용해서는 안 됩니다.
BIOS는 현존하는 가장 잘 알려진 펌웨어 중 하나입니다.
펌웨어란 다음과 같은 소프트웨어를 의미합니다:
OS/부트로더보다 먼저 실행되어 매우 낮은 수준의 설정을 수행합니다.
일반적으로 공급업체에서 제공하며 비공개 소스(closed source)이며, 문서화되지 않은 하드웨어 API와 상호 작용합니다.
OS/부트로더에 인터페이스를 제공하여 빠르고 간편한 I/O 같은 작업을 수행할 수 있게 합니다.
운영체제(OS)와 구별되는 점은 보통 더 작다는 것만 제외하고는 구분할 수 없습니다.
BIOS는 오래되고, 비표준화되었으며, x86 환경에서 어디에나 존재하지만 기능이 제한적입니다.
진지한 OS를 개발한다면 BIOS 기능을 가능한 한 적게 사용하는 것이 좋습니다.
BIOS 함수들은 모두 int 명령어를 통해 접근합니다:
mov <함수-ID>, %ah int <인터럽트-ID>
함수의 인자(argument)는 다른 레지스터에 저장됩니다.
인터럽트 ID는 전통적으로 16진수로 다음과 같습니다:
10h (이는 0x10과 동일합니다).
각 인터럽트-ID는 유사한 기능을 가진 여러 함수들을 그룹화합니다. 예를 들어, 10h는 비디오 관련 기능들을 포함하는 함수들을 그룹화합니다.
참고 문헌:
공식 문서나 표준화된 자료가 있습니까?
단일 @ 문자 출력:
./run bios_putc
새 줄(newline) 출력:
./run bios_newline
결과:
hello world
옛날처럼 캐리지 리턴(Carriage returns)이 필요합니다:
./run bios_carriage_return
결과:
hello world
현재 커서 위치 변경:
./run bios_cursor_position
결과:
cb
주어진 색상으로 문자 N번 쓰기:
./run bios_color
결과:
bcd
여기서:
b와 c는 전경색(foreground)이 빨강, 배경색(background)이 초록인 경우이고,
d는 기본 색상(검은색 바탕에 회색)입니다.
배경색을 전체 화면의 빨간색으로 변경하고 a 문자를 출력합니다:
./run bios_background
화면 스크롤:
./run bios_scroll
결과:
a c GG d
여기서 G는 빈 초록색 사각형입니다.
작동 방식:
스크롤 전:
a b c d
그런 다음 우리는 cx와 dx로 주어진 모서리 (1, 1)과 (2, 2)를 가진 직사각형에 대해 작동하기로 선택합니다:
a XX YY d
그리고 그 직사각형을 한 줄 위로 스크롤합니다.
Y는 채우기 색상인 초록색으로 채워집니다.
스크롤의 부분집합:
./run bios_clear_screen
결과:
b
빨간색 전경(foreground)에, 그리고 전체 화면을 녹색 배경(background)으로 설정하고, 초기 SeaBIOS 메시지 없이 실행합니다.
./run bios_pixel
이것을 보려면 조금 애를 써야 할 수도 있습니다.
이러한 픽셀로 선을 그립니다:
./run bios_pixel_line
고급 그래픽!
키보드를 통해 사용자로부터 한 문자를 받아, 그것을 하나 증가시키고 화면에 출력한 다음 정지합니다:
./run bios_keyboard
여러 문자를 입력하고 화면에 나타나는 것을 확인해 보세요:
./run bios_keyboard_loop
Ctrl-key 조합도 시도해 보세요.
int 13h를 사용하여 디스크에서 스테이지 2(stage 2)를 로드하고 실행합니다:
./run bios_disk_load
결과:
a
이 문자는 스테이지 2에서 출력되었습니다.
단지 하나가 아닌 두 개의 섹터를 로드해 보세요:
./run bios_disk_load2
결과:
ab
여기서 a는 첫 번째 블록의 코드에서, 그리고 b는 두 번째 블록의 코드에서 출력되었습니다.