본문 바로가기

[Debugging] Tips

[GDB] Arm(Armv7): C 코드와 어셈블리 코드를 동시에 디버깅하기

이번 포스트에서는 GDB를 사용해 디버깅을 하는 방법을 소개합니다. 소개된 내용을 참고하면 즐겁게 어셈블리 명령어를 디버깅할 수 있습니다.
 
환경: 라즈베리 파이4
 
GDB 실행
 
아래 명령어를 사용해 GDB를 Text User Interface 모드로 실행합니다.
 
$ gdb -tui armv7_aapcs_proc
 
다음은 위 명령어로 실행한 화면입니다.
 
 
이제 바로 다음 명령어를 입력해 어셈블리 명령어 창을 보이도록 합시다.
 
$ layout split
 
 
 
이번에는 'b main'와 'r' 명령어를 입력해 main() 함수에 브레이크 포인트를 걸고 프로그램을 실행합니다.
 
 
 
위 명령어를 입력하면 다음과 같은 화면이 보입니다.
 
 
 
위 화면과 같이 main() 함수의 첫 번째 라인에 브레이크 포인트가 걸립니다.
 
C 코드 한 줄 실행
 
자, 이어서 'n' 명령어를 입력해 C 코드 기준으로 소스 한 줄을 실행합시다.
 
 
C 코드 기준으로는 19번째 줄, 어셈블리 코드 기준으로 0x10490 주소까지 실행됐습니다.
 
어셈블리 명령어 기준 실행
 
이번에는 'si' 명령어를 입력해 어셈블리 코드를 라인 바이 라인(line-by-line)으로 실행합시다.
 
 
특이한 점은 어셈블리 코드 기준으로 브레이크 포인트가 움직이는데, C 소스 기준으로 브레이크 포인트는 변하지 않을 때가 많다는 점입니다. 이는 C 코드 1줄을 2~3개 어셈블리 명령어로 실행하기 때문입니다.
 
레지스터 창 출력하기 
 
이번에는 레지스터 정보를 확인하기 위해 'info reg' 명령어를 입력합시다.
 
 
SP(스택 포인터) 레지스터는 0xbefff5d0, LR(링크 레지스터)는 0xb6e6a718 주소를 저장합니다.
 
레지스터를 확인하기 위해 'layout reg' 명령어를 입력해 보겠습니다.
 
 
레지스터와 어셈블리 명령어가 출력됩니다. 이 상태로 계속 디버깅을 할 수 있으나, C 코드가 보이지 않으니 조금 답답합니다. 이 때 'layout split' 명령어를 다시 입력하면 C 코드와 어셈블리 명령어를 같이 볼 수 있는 창으로 변환됩니다.
 
이번에도 'si' 명령어를 입력해 어셈블리 명령어 'ldr    r0, [r11, #-8]'를 실행합니다.
 
 
이제 'bl     0x10434 <add_func>' 명령어를 실행해 add_func() 함수를 호출하기 직전의 시점입니다.
 
Armv7 아키텍처의 동작 원리를 배우며 디버깅해보기
 
여기서 한 가지 눈여겨 볼 부분이 있습니다. 'bl     0x10434 <add_func>' 명령어 아랫 부분의 주소가 0x104a4인데 add_func() 함수를 호출하면 복귀할 주소입니다.
 
그런데 add_func() 함수가 실행해 add_func() 함수의 시작 주소로 PC가 바뀌면 lr은 0x104a4 주소로 바뀔 것입니다. 이 점을 예상하고 'bl     0x10434 <add_func>' 명령어를 실행하기 직전의 레지스터 정보를 확인해 봅시다. (이를 위해 'info reg' 명령어를 입력했습니다.)
 
확인하니 lr(r14) 레지스터는 0xb6e6a718를 담고 있습니다.
 
이번에는 si 명령어를 입력해 'bl     0x10434 <add_func>' 명령어를 실행하겠습니다.
 
 
위 화면과 같이 add_func() 함수의 첫 번째 코드에 브레이크 포인트가 걸려 있습니다.
 
자, 이 시점에서 lr(r14) 레지스터를 확인해볼까요? 확인하니 lr(r14) 레지스터는 0xb6e6a718를 담고 있습니다.
 
이 방법을 활용해 GDB를 실행하면, Arm 어셈블리 명령어를 실행하면 레지스터가 어떻게 바뀌는지 직접 눈으로 확인할 수 있습니다.
 
Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자