• Wonhyuk Yang

[Practice] WAIOS 제작 (4)

Intro

저번 시간에는 고수준 입출력 함수 printk를 구현했었어요. qemu를 통해 빌드된 이미지를 실행시키면 정상적인 경우라면 "Hello world!"같은 printk에 입력으로 준 문자열이 출력이 될 거에요. 하지만 아무것도 출력되지 않는다면 어디선가 문제가 발생했다는 것이지요. 그러면 우리의 cpu는 어디서 무엇을 하고 있는 것일까요? GDB를 통해 어디서 무엇때문에 이러는 것인지 알아보도록 할게요.


Phase 1

아래의 커맨드를 통해 qemu를 디버거에 연결할 수 있도록 해줘요.

$ qemu-system-aarch64 -M virt -kernel Image -S -s -nographic -cpu cortex-a57 -serial mon:stdio

각 옵션들은 다들 아시겠지만 다시 설명해드리면,

  • -M virt: machine 옵션으로 Virt 보드를 선택함.

  • -kernel Image: 커널 옵션으로 Image를 커널 이미지로 사용.

  • -S: 에뮬레이션을 시작할 때 cpu 멈추도록 함.

  • -s: TCP 1234 포트를 통해 gdb를 연결할 수 있도록 함.

  • -nographic: GUI를 사용하지 않음.

  • -cpu cortex-a57: 에뮬레이터에 사용할 cpu를 cortex-a57로 선택.

  • -serial mon:stdio: 에뮬레이터의 시리얼 모듈을 콘솔의 출력으로 연결.

와 같아요. 그리고 아래 커맨드를 통해 gdb qemu와 연결하도록 할게요.

$ gdb -ex "target remote:1234" -ex "display/10i \$pc"

연결에 성공한 후 continue 커맨드를 입력하면, cpu는 무언가를 열심히 하게 될 것이에요. 이 때 <Ctrl+c>으로 멈춰 세워보면 $pc는 0x200에 위치하고 있을 것이에요.


그러면 왜 $pc가 0x200에 있을까요? 그것은 바로 exception이 발생했기 때문이에요. 아래 표를 살펴보시면 힌트를 얻을 수 있어요.

Reset할 때 Vector table의 위치를 저장하는 VBAR_EL1 레지스터의 값은 0으로 설정돼요. 따라서 0x200에 $pc가 있다는 말은 Synchronous exception이 발생했다는 말이에요. Synchronous exception은 여러 이유로 발생하는데 정확한 이유를 알기위해서 0x200에 브레이크 포인트를 걸어 보도록 할게요. 그러면 exception이 발생하면 브레이크 포인트에 걸리겠지요?


브레이크 포인트를 설정한 후 다시 qemu를 실행하도록 할게요. gdb에서는 target remote:1234를 통해 다시 qemu에 연결하면 돼요. 브레이크 포인트에 걸렸다면, 살펴볼 레지스터가 있어요. 바로 ELR_EL1 레지스터에요. 아래의 커맨드처럼 레지스터의 값을 출력해보세요.

(gdb) print/x $ELR_EL1
$1 = 0x40081d9c

그러면 익숙한 주소(0x40000000)에 가까운 값이 나오는데요. 이 값의 의미는 해당 instruction을 실행하여 exception이 발생했다는 말이에요. exceptio이 발생하면 아래 그림처럼 pc는 ELR_EL 레지스터에 저장되고, pstate는 SPSR_EL 레지스터에 저장되요.

그렇다면 해당 주소에 어떤 인스트럭션이 exception을 만들었는지 살펴보도록 할게요. 아래의 커맨드를 통해 ELR_EL1이 가르키는 인스트럭션을 살펴 볼게요.

(gdb) x/5i $ELR_EL1
 0x40081d9c:  str     q0, [sp, #112]
 0x40081da0:  str     q1, [sp, #128]
 0x40081da4:  str     q2, [sp, #144]
 0x40081da8:  str     q3, [sp, #160]
 0x40081dac:  str     q4, [sp, #176]

인스트럭션을 보시면 q로 시작하는 레지스터가 있네요. 이 녀석들은 floating 연산에 사용되는 레지스터인데 우리는 아직 floating 연산 기능을 활성화하지 않았어요. 따라서 해당 인스트럭션을 실행하면 exception이 발생하는 것이지요.


어떤 주소에서 exception이 발생했는지는 알았지만 해당 주소가 어떤 주소인지는 모르는 상태에요. 좀 더 정확한 정보를 얻기 위해서는 심볼 정보들을 로드해야 해요. 일반적인 실행 상황과는 다르게 우리의 심볼 정보들은 전부 가상 주소를 기반으로 적혀 있어요. 따라서 물리 주소를 사용하는 현재 상황에서는 심볼 정보들을 바로 사용할 수 없어요. 따라서 gdb의 특별한 옵션을 이용하여 심볼 정보들들 재배치 할게요.


symbol-file [filename[ -o offset]]  

gdb에서는 위 명령어처럼 심볼 정보를 파일로부터 가져올 수 있어요. 이때 -o 옵션을 오프셋 값을 주면 각 섹션의 시작 주소마다 오프셋을 더해요. 그러면 이 기능을 이용하여 물리 주소에 심볼 정보들을 올릴 수 있는 것이지요!

자세한 설명은 이 링크를 참고하세요! Commands to Specify Files

가상 주소와 물리 주소의 offset을 계산하면 0x100004000000(=0x40000000-0xffff000000000000)와 같아요. 아래 커맨드에서는 이 값을 오프셋으로 주고 vmlinux 파일의 심볼 정보들을 읽어와요.

(gdb) symbol-file vmlinux -o 0x1000040000000

이제 다시 ELR_EL1가 가르키는 인스트럭션을 읽어보면 아래와 같이 심볼 정보도 같이 살펴볼 수 있어요.

(gdb) symbol-file vmlinux -o 0x1000040000000
Reading symbols from vmlinux...
(No debugging symbols found in vmlinux)
(gdb) x/20i $ELR_EL1
 0x40081d9c <printk+40>:      str     q0, [sp, #112]
 0x40081da0 <printk+44>:      str     q1, [sp, #128]
 0x40081da4 <printk+48>:      str     q2, [sp, #144]
 0x40081da8 <printk+52>:      str     q3, [sp, #160]
 0x40081dac <printk+56>:      str     q4, [sp, #176]
 0x40081db0 <printk+60>:      str     q5, [sp, #192]
 0x40081db4 <printk+64>:      str     q6, [sp, #208]
 0x40081db8 <printk+68>:      str     q7, [sp, #224]
 0x40081dbc <printk+72>:      add     x0, sp, #0x130
 0x40081dc0 <printk+76>:      str     x0, [sp, #72]
 0x40081dc4 <printk+80>:      add     x0, sp, #0x130
 0x40081dc8 <printk+84>:      str     x0, [sp, #80]
 0x40081dcc <printk+88>:      add     x0, sp, #0xf0
 0x40081dd0 <printk+92>:      str     x0, [sp, #88]
 0x40081dd4 <printk+96>:      mov     w0, #0xffffffc8           // #-56
 0x40081dd8 <printk+100>:     str     w0, [sp, #96]
 0x40081ddc <printk+104>:     mov     w0, #0xffffff80           // #-128
 0x40081de0 <printk+108>:     str     w0, [sp, #100]
 0x40081de4 <printk+112>:     add     x2, sp, #0x10
 0x40081de8 <printk+116>:     add     x3, sp, #0x48

그러면 이제 해당 주소가 printk에 속하는 것을 알 수 있네요. 가변 인자를 다루는 매크로를 사용했더니 floating 연산도 같이 하게 되었네요. 이제 이 문제를 해결해보도록 할게요.


해결 방안으로 두 가지 방법이 떠오르는데,

  • 첫 번째 방법으로는 floating point 연산을 활성화하는 것이에요.

Linux kernel의 head.S에서도 floating point 연산을 활성화시켜요. head.S 찍어먹기 (6)__cpu_setup 설명에 floating 연산 활성화 부분이 나와있어요.

  • 두 번째 방법으로는 gcc 옵션으로 floating point 연산 레지스터를 사용하지 않도록 하는 것이에요.

gcc의 AArch64 옵션 중에는 -mgeneral-regs-only가 있어요. 해당 옵션을 사용하면 컴파일러가 floating과 SIMD 레지스터의 사용을 막아요.


첫 번째 방법은 리눅스 커널 분석 때 보았으므로, 두 번째 방법을 사용할게요.


Phase 2

이제 문제를 해결했으니 동작해야 하는데 여전히 동작하지 않고 있네요... 이번엔 어떤 문제인지 살펴보도록 할게요. 이전과 동일한 방법으로 어디서 문제가 발생했는지 살펴보도록 할게요.


이전과 마찬가지로 ELR_EL1가 가르키는 인스트럭션을 덤프해보면 아래와 같아요.

(gdb) x/10i $ELR_EL1
 0x40081e2c <printk+184>:     .inst   0x00000001 ; undefined
 0x40081e30 <printk+188>:     .inst   0x40081e60 ; undefined
 0x40081e34 <printk+192>:     .inst   0x00000000 ; undefined
 0x40081e38 <printk+196>:     .inst   0x4008186c ; undefined
 0x40081e3c <printk+200>:     .inst   0x00000000 ; undefined
 0x40081e40 <printk+204>:     ldp     x29, x30, [sp], #176
 0x40081e44 <printk+208>:     ret

printk+184에 위치한 인스트럭션이 정의되지 않은 opcode라 exception이 발생했네요. 컴파일 자체에서 문제가 발생한 것일까요? vmlinux의 printk를 덤프하여 해당 위치를 살펴볼게요.

$ objdump -d vmlinux
...
ffff000000081e2c:       b9406be0        ldr     w0, [sp, #104]
ffff000000081e30:       6b00003f        cmp     w1, w0
ffff000000081e34:       54fffeab        b.lt    ffff000000081e08 
ffff000000081e38:       d503201f        nop
ffff000000081e3c:       d503201f        nop
ffff000000081e40:       a8cb7bfd        ldp     x29, x30, [sp], #176
ffff000000081e44:       d65f03c0        ret

컴파일 자체에는 문제가 없어 보이네요. 그러면 qemu에 로드할 때 문제가 발생한 것일까요? qemu를 다시 시작하고 브레이크 포인트가 잡혀 있을 때, 해당 위치를 덤프해볼게요.

(gdb) x/5i printk+184
 0x40081e2c <printk+184>:     ldr     w0, [sp, #104]
 0x40081e30 <printk+188>:     cmp     w1, w0
 0x40081e34 <printk+192>:     b.lt    0x40081e08 <printk+148>  // b.tstop
 0x40081e38 <printk+196>:     nop
 0x40081e3c <printk+200>:     nop

문제가 없는 것으로 보아 런타임 과정 중 해당 위치에 값이 덮어씌어지는 것 같아요. 그러면 왜/ 언제 값이 변경되는지 확인하기 위해 watchpoint를 설정해서 살펴보도록 할게요.

(gdb) watch *0x40081e2c
Hardware watchpoint 1: *0x40081e2c
(gdb) c
Continuing.

Hardware watchpoint 1: *0x40081e2c

Old value = -1186960416
New value = 0
0x0000000040081308 in strlen ()
(gdb) x/10i $pc-4
 0x40081304 <strlen+8>:       str     wzr, [sp, #44]
=> 0x40081308 <strlen+12>:      ldrsw   x0, [sp, #44]
 0x4008130c <strlen+16>:      and     x0, x0, #0xfffffffffffffff8
 0x40081310 <strlen+20>:      ldr     x1, [sp, #8]
 0x40081314 <strlen+24>:      add     x0, x1, x0
 0x40081318 <strlen+28>:      ldr     x0, [x0]
 0x4008131c <strlen+32>:      str     x0, [sp, #32]
 0x40081320 <strlen+36>:      str     wzr, [sp, #28]
 0x40081324 <strlen+40>:      b       0x40081364 <strlen+104>
 0x40081328 <strlen+44>:      ldr     x0, [sp, #32]

디버거에 나온 내용에 따르면 <strlen+8>에서 변경된 것으로 보여요. 해당 인스트럭션은 스택 포인터에 44를 더한 곳에 0을 저장하네요. 그렇다면, 스택 포인터가 text 영역을 넘어간 것으로 보이네요 실제로 값을 확인해보면 아래와 같아요.

(gdb) p $sp+44
$1 = (void *) 0x40081e2c <printk+184>

그러면 스택이 이상하게도 .text 섹션까지 침범해버렸네요. 혹시 링커 스크립터에서 스택을 잘못 설정한게 아닐가요? 한 번 살펴보도록 할게요.

유심히 살펴보니 어떤게 문제인지 알겠네요. 여러분도 알아채셨나요? stack을 위한 공간을 잡았지만 stack_top 심볼의 위치가 stack의 bottom에 있었네요! 이제 해당 부분을 수정하고 빌드한 후 이미지를 실행시켜보도록 할게요. 그러면 아래처럼 성공적으로 출력되는 것을 확인할 수 있네요 👏

Conclusion

이번 시간에는 문제점을 찾기 위해 GDB를 사용하였어요. 글로 디버깅 과정을 풀어쓰니까 작위적인 것 같지만, 실제로 저는 동일한 문제를 겪었고 해결하는 과정을 그대로 적은 것이에요😅 이러한 문제 진단 방법이 여러분들의 OS 개발에 도움이 되었으면 하네요. 해당 내용의 소스 코드는 아래 링크에 올렸습니다. 감사합니다.


Git repository

Phase 1: https://github.com/YWHyuk/WAIOS/tree/printk

Phase 2: https://github.com/YWHyuk/WAIOS/tree/06faeaf73e0f3281e27c3b7c9f0474f0d6cf232c


조회수 96회댓글 0개

관련 게시물

전체 보기