이번 시간에는 저번 시간에 만든 uart 드라이버를 이용하여, C 표준 입출력 스타일의 함수를 만들어보도록 할게요. print와 같이 고수준 함수들이 지원이 된다면 좀 더 쉽게 정보들을 덤프할 수 있고 여러가지 재미있는 일들을 할 수 있어요. 결과적으로, 구현의 목표 함수들은 아래와 같아요.
printk
atoi
vsprintk
Variodic function
printf 함수는 일반적인 함수와는 다르게 가변인자를 사용하는 함수에요. 따라서 헤더 파일에는 다소 생소하게 선언된 것을 확인할 수 있어요. "..."으로 표시하여 해당 함수는 가변인자를 받는다고 알려주고 있어요.
extern int printf (const char *__restrict __format, ...);
따라서, 우리만의 printk를 구현하려면 printf와 동일하게 가변인자를 처리할 수 있어야 해요. 가변인자를 처리하기 위해서 ISO C는 stdarg.h에 정의된 4가지 매크로와 한 가지의 자료형을 제공하고 있어요.
va_list
void va_start(va_list ap, last);
va_start() 매크로는 va_list를 초기화하는 매크로에요. 이 매크로가 먼저 실행된 후 va_arg, va_end가 실행되어야 해요. 두번째 인자 last는 가변인자 이전에 나온 가장 마지막 인자에요. 즉 위의 printf의 경우 가변 인자(...) 이전에 인자인 __format이 last가 될 것이에요.
type va_arg(va_list ap, type);
va_arg() 매크로는 ap가 가르키는 다음 인자를 type 형태로 반환해요. 또한, ap를 다음 인자의 위치로 이동해요. 따라서 va_arg를 연속적으로 사용하면, 연속해서 인자들을 얻을 수 있어요. va_start로 ap가 초기화된 직후 va_arg 매크로가 사용되면 last 다음에 위치한 인자가 반환될 것이에요.
void va_end(va_list ap);
ap 사용 종료 후 사용되는 매크로에요.
void va_copy(va_list dest, va_list src);
va_copy() 매크로는 초기화된 가변인자 src에서 dest로 복사하는 매크로에요.
이제 간단한 가변인자 함수 예제를 살펴보도록 할게요.
해당 함수는, 포맷 스트링을 받아서 형식에 따라 문자열, 정수, 캐릭터로 출력해주는 함수에요.
Line 11에서 va_start를 통하여 va_list ap를 초기화 해줘요.
Line 15, 19, 25에서는 포맷 스트링 유형에 따라 타입을 결정하여 값을 읽어와요.
Line 29에서는 va_list ap의 사용을 종료해요.
Simple printk 구현
이제 위의 예제와 같은 방식으로 vsprintk를 구현해보도록 할게요. 대신 vsprintf처럼 인자로 va_list를 받도록 할게요.
저는 위와 같이 문자, 문자열, 정수(10진수, 16진수)를 지원하는 함수로 구현했어요. 버퍼의 사이즈를 체크하지 않는다는 문제가 있긴 하지만 넘어가도록 할게요. 정수를 string으로 변환해주는 부분은 itoa 함수가 해주는데요, 소소한 재미를 위해 이 함수의 코드는 첨부하지는 않을게요.
이제 vsprintk를 호출하고 uart를 통해 출력해주는 printk를 구현해봐요. 제가 구현한 printk는 아래와 같아요.
Line 4에서 vsprintk에 전달할 버퍼를 static하게 선언했어요.
Line 7~9에서는 가변인자를 초기화한 후 vsprintk에 전달해줘요.
Line 11~13에서는 버퍼에 문자열을 uart를 통해 출력해요.
해당 포스트에서는 위와 같이 코어 부분만 다루고 이외에 디테일한 부분들은 전부 여러분들에게 맡기도록 할게요.
자 이제 main 함수에서 printk를 통해 "Hello world!"를 출력하도록 변경해볼게요.
이제 컴파일하면 동작할까요? 아쉽게도 오류가 발생할거에요. 왜냐하면, main에 buffer와 printk의 static변수인 buffer은 .text 섹션에 위치하지 않아요. 따라서 기존의 링커 스크립트에 rodata, data, bss 섹션들을 포함시켜주도록 할게요.
이제 빌드한 vmlinux 파일을 readelf -s 옵션으로 살펴보면 static 변수 buffer가 bss 섹션에 포함되어 있는 것을 확인할 수 있어요.
$ readelf -s vmlinux
...
29: ffff00000008c000 2048 OBJECT LOCAL DEFAULT 4 buffer.1818
혹시 심볼이 보이지 않나요? printk.o 오브젝트 파일을 빌드할 때 추가해주셨나요?
또한 readlef -S으로 vmlinux의 섹션 정보들을 조회하면 data, bss, rodata 섹션들이 포함되어 있는 것을 확인할 수 있어요.
$ readelf -S vmlinux
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .head.text PROGBITS ffff000000080000 00010000
0000000000000040 0000000000000000 AX 0 0 4
[ 2] .text PROGBITS ffff000000081000 00011000
0000000000000e48 0000000000000000 AX 0 0 4
[ 3] .data PROGBITS ffff000000081e48 00011e48
00000000000041b8 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS ffff000000086000 00016000
0000000000000800 0000000000000000 WA 0 0 8
[ 5] .rodata PROGBITS ffff000000086800 00016800
0000000000000052 0000000000000000 A 0 0 8
...
그런데, 아직 page table이 세팅이 안되었는데 main.c에서 rodata에 위치한 문자열들을 사용해도 되는 것일까요? 저도 궁금해서 덤프를 해봤어요. 덤프한 결과는 아래와 같아요.
ffff000000081198 <start_kernel>:
ffff000000081198: a9be7bfd stp x29, x30, [sp, #-32]!
ffff00000008119c: 910003fd mov x29, sp
ffff0000000811a0: b0000020 adrp x0, ffff000000086000
ffff0000000811a4: 91200000 add x0, x0, #0x800
ffff0000000811a8: f9000fe0 str x0, [sp, #24]
ffff0000000811ac: 97ffff98 bl ffff00000008100c <init_uart>
ffff0000000811b0: 52800022 mov w2, #0x1
ffff0000000811b4: f9400fe1 ldr x1, [sp, #24]
ffff0000000811b8: b0000020 adrp x0, ffff000000086000
ffff0000000811bc: 91204000 add x0, x0, #0x810
ffff0000000811c0: 940002ed bl ffff000000081d74 <printk>
ffff0000000811c4: 14000000 b ffff0000000811c4
start_kernel에서 rodata를 접근할 때, pc-relative한 addressing을 사용하고 있네요.빨간색으로 표시된 부분이 printk의 인자인 포맷 스트링을 가져오는 부분이에요. 보시면 adrp와 add를 통해서 문자열들의 주소를 가져오고 있어요. 따라서 현재로써는 printk를 실행하는 코드는 mmu가 비활성화 되어있어도 동작해요.
그러면 링커 스크립트도 수정했으므로, 이제 실행하면 될까요? 아쉽게도 출력되지 않을거에요... 아쉬움을 뒤로한 채 다음에 이어서 어떻게 디버깅을 하는지 알아보고 왜 동작하지 않는지를 분석해보도록 할게요. 제가 작성한 코드는 아래의 링크를 참고하세요
Comments