TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 32 READS

운영체제 없이 Swift로 커널 짜기: QEMU 위에서 'Hello world'까지

애플리케이션 개발에 익숙한 사람일수록 운영체제가 얼마나 많은 일을 대신 해주는지 잊기 쉽다. 표준 출력에 문자를 찍는 것조차 커널과 C 런타임, 디바이스 드라이버가 뒤에서 움직인 결과다. 최근 공개된 한 실험 프로젝트는 그 추상화를 걷어내고, Swift로 작성한 아주 작은 커널을 QEMU 에뮬레이터 위에서 직접 부팅시키는 과정을 기록했다. 목표는 리눅스를 대체하는 것이 아니라, '운영체제가 없는 상태에서 프로그램을 실행하려면 무엇이 필요한가'를 손으로 더듬어 확인하는 데 있다. 지금 이 커널이 하는 일은 메시지 한 줄을 출력하고 무한히 대기하는 것이 전부지만, 그 한 줄을 찍기까지 거쳐야 하는 단계가 이 글의 핵심이다.

Embedded Swift와 베어메탈 타깃

프로젝트는 Embedded Swift를 사용한다. 이는 운영체제도 없고 통상적인 의미의 표준 라이브러리도 없는 환경을 겨냥한 Swift의 부분집합이다. 다만 아직 실험적 기능이라 공개 정식 릴리스에는 포함되지 않아, 저자는 개발용(development) 툴체인을 따로 설치한 뒤에야 작은 프로그램을 컴파일하고 실행할 수 있었다. 이것만으로도 Embedded Swift가 최소 환경에서 돈다는 사실은 확인된 셈이다.

다음 단계는 운영체제가 전혀 없는 기계를 겨냥하는 것이다. 저자는 애플 실리콘 맥에서 QEMU의 virt 머신을 띄우고, 타깃으로 aarch64-none-none-elf를 지정했다. 이 '트리플'은 64비트 ARM이면서 운영체제도, C 런타임이 제공하는 환경도 없는 기계를 뜻한다. QEMU가 부팅되면 CPU는 날것(raw) 상태이므로, Swift 코드를 안전하게 실행하기 전에 스택을 구성하고 프로그램의 시작 지점을 정해줘야 한다. 그래서 최소한의 어셈블리 부팅 파일(boot.S)이 필요하다. 이 코드는 QEMU가 띄울 수 있는 여러 가상 코어 중 첫 번째 코어만 커널을 실행하게 하고, 나머지 코어는 wfe 명령으로 오지 않을 이벤트를 기다리며 멈춰 있게 한다.

런타임이 존재하기 위한 최소 조건

빌드는 곧바로 되지 않았다. 링커가 처음 실패한 이유는 명확하다. 베어메탈 타깃에는 운영체제도 C 표준 라이브러리도 없으니 putchar도, memmove도 존재하지 않는다. 결국 저자는 이 저수준 함수들을 직접 구현한다. putchar는 QEMU virt 머신에서 0x09000000에 매핑된 PL011 UART 레지스터에 문자를 직접 써넣는 방식이다. 화면 드라이버가 아니라 시리얼 출력 드라이버이며, -nographic 옵션으로 실행하면 QEMU가 UART로 받은 바이트를 터미널에 그대로 보여준다. memmove는 소스와 목적지가 겹치는지에 따라 앞 또는 뒤로 복사하는 간단한 구현을 붙였다.

저자가 짚은 대목이 흥미롭다. 이 지점에서 작업은 더 이상 평범한 애플리케이션 프로그래밍이 아니게 된다. 문자열을 찍기 위해 운영체제 API를 부르는 것이 아니라, 언어 런타임이 '존재하기 위해' 필요로 하는 가장 낮은 수준의 함수를 스스로 공급하고 있기 때문이다. 또 한 가지, Swift 쪽 진입점은 일반적인 main 함수가 아니라 @c 속성을 붙여 C 호환 이름으로 노출한 커널 엔트리 함수다. 이렇게 해야 어셈블리 부팅 코드가 그 이름으로 Swift 함수를 호출할 수 있다. 참고로 이 글은 애초 @_cdecl를 쓰던 것을, @c 속성이 동작한다는 제보를 받아 수정했다고 밝힌다. @c는 전역 함수를 Swift로 구현한 C 함수로 표시하는, @_cdecl를 공식화한 속성이다.

링커 스크립트가 지도를 그린다

코드가 준비돼도 링커는 커널이 어느 주소에 적재되고 실행 파일의 각 부분이 어디에 놓이는지를 알아야 한다. 이 역할을 linker.ld가 맡는다. 이 설정에서 QEMU virt 머신은 AArch64 커널을 0x40080000에 적재하며, 스크립트는 부팅 코드를 맨 앞에 두고 읽기 전용 데이터, 초기화된 데이터, 미초기화 데이터, 스택 순으로 배치한다. 어셈블리는 __stack_top 심벌로 스택 포인터를 초기화하는데, 현재 스택은 128KB다. 문장 하나 찍는 커널치고는 과한 크기지만 실험 단계에서 Swift가 함수를 호출할 여유를 주려는 선택이다. 첫 링크에서 터졌던 '고아 섹션(orphan section)' 오류도 여기서 해결된다. 고아 처리가 켜진 링커는 ELF 메타데이터를 어디에 둘지 추측하지 않으려 하므로, 유지할 섹션들의 위치를 스크립트에 명시해야 한다.

결과물은 결코 쓸 만한 커널은 아니지만, 진짜 AArch64 ELF 실행 파일이다. 어셈블리 진입점에서 시작해 표준 라이브러리 없이 링크되고, 수동으로 구성한 스택 위에서 Swift 코드를 호출한다. 여기서 한 발 더 나아가 저자는 단방향 출력을 양방향으로 바꾼다. PL011 UART의 수신 레지스터를 읽기 전에 플래그 레지스터의 4번 비트(RXFE)로 수신 FIFO가 비었는지 확인하고, 비어 있으면 기다리는 방식이다. 이렇게 하면 입력한 문자를 터미널로 되돌려 보내는 에코가 가능해지고, > 기호와 공백(ASCII 62와 32)을 찍어 간단한 프롬프트 모양까지 만들 수 있다.

실무자가 가져갈 것과 명확한 한계

이 프로젝트의 가치는 완성도가 아니라 경계면을 드러낸 데 있다. 평소 당연하게 여기는 표준 출력, 메모리 복사, 진입점 같은 요소가 운영체제 아래에서는 전부 직접 공급해야 하는 계약이라는 사실을, 코드 레벨에서 구체적으로 보여준다. Swift 같은 고수준 언어도 런타임이 기대하는 최소 함수만 채워주면 베어메탈에서 돌 수 있다는 점은, 임베디드나 펌웨어 쪽을 들여다보는 개발자에게 특히 참고할 만하다.

다만 한계도 분명하다. 입력 처리는 인터럽트가 아니라 폴링이라 CPU가 문자 하나하나를 기다리며 계속 레지스터를 확인하는 매우 비효율적인 방식이고, 저자도 실제 커널이라면 결국 인터럽트를 써야 한다고 인정한다. 라인 편집도, 백스페이스 처리도, 명령 파서도 없다. Embedded Swift 자체가 아직 실험적이어서 정식 툴체인이 아닌 개발 빌드를 써야 하는 점도 재현을 시도할 때 감안해야 한다. 저자는 이 지점을 TODO 목록을 시작할 좋은 멈춤 자리로 삼았고, 이후 방향으로 프레임버퍼, 실제 키보드 드라이버, 최소한의 대화형 환경을 언급한다. 그 전에 밑바닥 기계를 이해하는 것이 먼저라는 태도가, 이 작은 실험이 전하는 실질적 교훈에 가깝다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://carette.xyz/posts/minimal_swift_kernel_on_qemu/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...