Wave에는 인라인 어셈블리라는 기능이 존재하는데 오늘은 Wave에서 인라인 어셈블리를 이용해 BIOS를 직접 호출해 봤습니다. int 0x10 인터럽트는 실모드에서 가장 기본적인 비디오 출력 기능인데,
AH 레지스터에 0x0E를 넣고, AL 레지스터에 출력할 문자를 넣은 뒤 int 0x10을 호출하면
화면에 문자가 출력됩니다.
이 방법을 이용해 "Hi!"를 출력하고, CR(0x0D)과 LF(0x0A)로 줄바꿈을 한 뒤 "OK"를 출력했습니다.
일단 Wave 컴파일러는 현재 리눅스에서만 빌드가 가능하며, 기본적으로는 실행 가능한 바이너리만 생성합니다. 따라서 부트로더처럼 바로 .img 디스크 이미지를 만들어주지는 않습니다. 다만 Wave가 컴파일될 때 /target 폴더가 생성되는데, 그 안에는 LLVM IR 파일(.ll)과 리눅스용 바이너리가 함께 생성됩니다.
우리는 BIOS용 코드를 만들 것이기 때문에 리눅스 바이너리는 필요하지 않고, 대신 LLVM IR 코드가 담긴 temp.ll 파일을 활용하게 됩니다.
Wave는 현재 프론트엔드 개발 단계에 있으며, 테스트를 위해 임시로 LLVM을 이용해 개발하고 있습니다. 이 LLVM은 우선 LLVM IR을 생성하며, 이 IR은 clang 명령어를 통해 원하는 타겟 아키텍처의 오브젝트 파일이나 실행 파일로 변환할 수 있습니다.
fun main() {
// BIOS teletype 모드로 'H' 출력
asm {
"mov ah, 0x0e" // AH = 0x0E → BIOS int 0x10 문자 출력 모드
"mov al, 0x48" // AL = 0x48 → ASCII 문자 'H'
"int 0x10" // BIOS 비디오 서비스 호출 → 'H' 출력 후 커서 이동
}
// BIOS teletype 모드로 'i' 출력
asm {
"mov ah, 0x0e" // AH = 0x0E → 문자 출력 모드 유지
"mov al, 0x69" // AL = 0x69 → ASCII 문자 'i'
"int 0x10" // BIOS 호출 → 'i' 출력
}
// BIOS teletype 모드로 '!' 출력
asm {
"mov ah, 0x0e" // AH = 0x0E → 문자 출력 모드 유지
"mov al, 0x21" // AL = 0x21 → ASCII 문자 '!'
"int 0x10" // BIOS 호출 → '!' 출력
}
// 줄바꿈 처리 (Carriage Return: CR)
asm {
"mov ah, 0x0e" // AH = 0x0E → 문자 출력 모드
"mov al, 0x0D" // AL = 0x0D → CR(Carriage Return), 커서를 맨 앞으로 이동
"int 0x10" // BIOS 호출 → 커서 열 위치 0으로 이동
}
// 줄바꿈 처리 (Line Feed: LF)
asm {
"mov ah, 0x0e" // AH = 0x0E → 문자 출력 모드
"mov al, 0x0A" // AL = 0x0A → LF(Line Feed), 한 줄 아래로 이동
"int 0x10" // BIOS 호출 → 줄바꿈 완료
}
// BIOS teletype 모드로 'O' 출력
asm {
"mov ah, 0x0e" // AH = 0x0E → 문자 출력 모드
"mov al, 0x4F" // AL = 0x4F → ASCII 문자 'O'
"int 0x10" // BIOS 호출 → 'O' 출력
}
// BIOS teletype 모드로 'K' 출력
asm {
"mov ah, 0x0e" // AH = 0x0E → 문자 출력 모드
"mov al, 0x4B" // AL = 0x4B → ASCII 문자 'K'
"int 0x10" // BIOS 호출 → 'K' 출력
}
}
이제 이 코드를 BIOS 환경에서 실행하기 위해 빌드 과정을 거쳐야 합니다.
앞서 설명했듯 Wave 컴파일러는 .img를 직접 마늗ㄹ어주지 않기 때문에,
우리는 temp.ll 파일을 이용해 수동으로 부트 이미지를 만들어야 합니다.
빌드 순서는 다음과 같읍니다.
Wave 코드 -> LLVM IR(temp.ll) 생성
wavec run main.wave
LLVM IR -> 16비트용 오브젝트 파일로 변환
llc -march=x86 -mattr+16bit-mode -filetype=obj target/temp.ll -o boot.o
오브젝트 파일 -> 부트로더 바이너리로 링크
ld -m elf_i386 -Ttext 0x7c00 --oformat binary boot.o -o boot.bin
부트섹터 시그니처(0x55AA) 추가
echo -ne '\x55\xAA' | dd of=boot.bin bs=1 seek=510 count=2 conv=notrunc
최종 부트 이미지 생성
dd if=boot.bin of=os.img bs 512 count=1 conv=notrunc
QEMU로 실행
qemu-system-i386 -drive format=raw,file=os.img
그리고 이걸 자동화하기 위해 build.sh 스크립트를 만들어두면 편리합니다.
#!/bin/bash
set -e
LL_FILE=target/temp.ll
OBJ_FILE=boot.o
BIN_FILE=boot.bin
IMG_FILE=os.img
wavec run main.wave
llc -march=x86 -mattr=+16bit-mode -filetype=obj $LL_FILE -o $OBJ_FILE
ld -m elf_i386 -Ttext 0x7c00 --oformat binary $OBJ_FILE -o $BIN_FILE
echo -ne '\x55\xAA' | dd of=$BIN_FILE bs=1 seek=510 count=2 conv=notrunc
dd if=$BIN_FILE of=$IMG_FILE bs=512 count=1 conv=notrunc
echo "[+] Image created: $IMG_FILE"
이제 ./build.shfh dlalwlfmf aksemfrh,
qemu-system-i386 -drive format=raw,file=os.img
을 실행하면 QEMU에서 Hi!와 줄바꿈된 OK가 출력되는 걸 확인할 수 있습니다.

여기까지는 Wave에서 인라인 어셈블리를 이용해 BIOS를 직접 호출하고 간단한 문자열을 출력하는 방법을 살펴봤습니다.
이번 예제는 아주 기본적인 문자 출력 기능만 사용했지만,
같은 방식으로 int 0x16(BIOS 키보드 입력) 같은 다른 BIOS 인터럽트도 호출할 수 있습니다.
또한 VGA 메모리에 직접 접근하거나, 간단한 부트로더 로직으로 확장하는 것도 가능합니다.
Wave는 아직 개발 초기 단계이지만, 인라인 어셈블리를 통해 BIOS 호출이 가능하다는 점에서
저수준 개발에도 충분히 활용할 수 있는 가능성을 보여줍니다.
앞으로는 키보드 입력, VGA 메모리 출력 등으로 확장하면서 더 재미있는 실험을 해볼 예정입니다.
Github: https://github.com/LunaStev/Wave
Website: https://wave-lang.dev