IT/Linux Kernel

디버깅을 통해 배우는 리눅스 커널의 구조와 원리 1, 도서 공부하기 18 - 프로세스 디버깅, gdb로 glibc 디버깅

변화의 물결1 2025. 4. 30. 00:03

 

 

안녕하세요.

 

책에서 부분 분량이 조금 있다 보니, 글에 다 담기는 어려울 것으로 생각됩니다. 그래서 필요하다고 생각하는 부분을 요약해 보았습니다.

 


 

 

1. glibc의 fork() 함수를 gdb로 디버깅

 

1) 실습을 위한 예제 코드작성

 

 

 프로세스 디버깅하기 위한 app 소스코드에 fork() 함수한 새로운 프로세스를 생성해서 부모 프로세스와 자식 프로세스로 만들어 확인합니다.

 

 코드는 핵심적인 부분만 확인하겠습니다. 전체 소스코드는 글 마지막에 첨부했습니다.

 26번 줄에서 fork() 함수를 호출해 반환값을 pid에 저장합니다.

 pid가 0이면 자식프로세스로 0보다 크면 부모 프로세스가 생성되었다고 판단합니다.

 fork() 함수 실행 시 오류가 발생하면 -1을 반환합니다.

 

 

 

 Makefile을 만들어 아래 내용을 입력하고, 컴파일해서 실행파일을 만듭니다. 옵션에서 -g를 추가해 디버깅을 위한 옵션을 선택합니다.

 

raspberry_fork: raspberry_fork_test.c
         gcc -g -o raspberry_fork raspberry_fork_test.c

 

 

make로 실행파일을 만든 후 실행하면 아래와 같은 결과를 볼 수 있습니다.

 

  

 

2) gdb 프로세스 생성 과정 디버깅하기

 

 이제 생성한 파일을 과정을 디버깅해 보겠습니다. 먼저 gdb를 실행합니다.

 

 4_11_app# gdb raspberry_fork

 

 

 

 "b main" 명령어로 main() 함수에 브레이크포인터(Break point)를 설정합니다. "r" 명령어를 입력해서 gdb 프로그램을 실행합니다.

 

 

 

 위 상태가 브레이크포인트에 멈춘 상태이며, 여기서부터 gdb로 디버깅할 수 있습니다. "list"명령어로  소스코드를 확인합니다.

 

 

 

 "n"명령어로 소스코드를 라인단위로 확인할 수 있고, "s"명령어로 함수 내부로 진입할 수 있습니다. "n"을 한번 입력하면 fork() 코드로 이동하고, fork() 부분에서 "s" 명령어를 눌러 함수 내부로 진입합니다.

 

 

 

 결과가 생각하고 다르게 No such file or directory라고 표시됩니다. __libc_fork() 함수는 sysdeps/nptl/fork.c 에 있는데 현재 gdb환경에서 sysdeps/nptl/fork.c 소스코드가 없다고 해석하면 됩니다.

 

 보통 gdb에서 라이브러리 파일에 내부에 진입해서 디버깅 시 볼 수 있는 메시지이며, 이 라이브러리를 컴파일한 소스코드 위치를 알려줍니다.

 

 라이브러리는 C형식의 소스코드는 없지만 어셈블리 코드는 확인할 수 있습니다. "layout asm" 명령어를 입력해 gdb의 소스코드 출력 설정을 어셈블리 코드 형식으로 바꿉니다.  (다시 C 형식으로 바꾸려면, "layout src"라고 하면 됩니다.)

 

 상단 ">" 0xb6ef43e4 주소 부분이 브레이크포인트가 걸린 위치입니다.

 

 

 

 조금 복잡해지긴 하는데요, 어셈블리 코드의 실행 흐름을 디버깅하려면 ARM 레지스터가 어떻게 바뀌는지도 알아야 합니다. 추가적으로 "layout reg" 명령어를 입력하면 레지스터 정보도 나타납니다.

 

 lr(r14)는 복귀 레지스터이며 0x104d4로 되어 있습니다.

 

 

 

 "nexti"명령어를 입력해서  어셈블리 명령어 단위로 단계별 실행되는 것을 확인할 수 있습니다. push 명령이 실행된 후 sp(r13)와 pc레지스터가 변경된 것을 알 수 있습니다.

 

 

 

 소프트웨어 인터럽트를 발생시켜 시스템 콜을 실행하기 직전 코드에 브레이크포인터를 겁니다. "svc 0x00000000" 코드의 명령어가 실행되면 유저 모드에서 커널 모드로 스위칭되는 것입니다.

 

 

 

 위 내용을 토대로 시스템콜을 실행하기 직전 어떤 인자로 시스템 콜을 호출하는지 확인할 수 있습니다.

 (svc 0x00000000 지점에서 nexti 진행하지 하지 않고 마우스 스크롤로 내려보면 다음 코드를 볼 수 있습니다.)

 

 0xb6ef442c ... fork+72 줄을 보면 ldr명령어로 0xb6ef45a0 주소의 값을 r0 레지스터에 저장합니다. 0xb6ef45a0 주소를 보면 0x01200011이 있습니다.

 

 0x01200011 값은 이전 내용에서 한번 다루었습니다. 매크로를 OR 연산한 값입니다. 그럼 어떤 값인가 하면, 아래와 같습니다. 즉, 자식프로세스의 스레드 아이디를 설정하고 자식 프로세스를 생성한다는 플래그입니다.

 

linux# vim include/uapi/linux/sched.h

 

 

 

linux# vim arch/arm/include/uapi/asm/signal.h

 

 

 

0x01200011 = CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD

 

 

 

 위의 내용의 0xb6ef4430 ... fork+76 줄을 좀 더 보면 r7레지스터에 120을 저장합니다. 이는 시스템 콜을 발생하기 전에 유저 공간에서 r7레지스트에 시스템 콜 번호를 지정합니다.

 유저 공간에서 fork함수를 호출하면 커널 공간에서 sys_clone() 함수가 호출되는 이유입니다.

 

 0xb6ef4434 ... fork+80 줄은 ARM 프로세서에서 소프트웨어 인터럽트를 발생하는 명령어입니다. svc는 Superviser Call을 의미하며 ARM 프로세서의 프로그램 카운터를 소프트웨어 인터럽트 백터인 vector_swi로 브랜치 합니다.

  

 슈퍼바이저 모드로 스위칭하고 vector_swi 레이블을 실행합니다.

 이 부분은 책에 나온 부분과 약간 소스코드가 다른 부분이 있습니다. 기능이 변경되었다는 것이 아니라 형식이 다르게 되어 있습니다.

 

linux# vim arch/arm/kernel/entry-common.S

 

 

 

 svc 명령어를 실행하기 직전 유저 공간에서 실행 중인 레지스터는 커널 공간의 소프트웨어 인터럽트 백터인 vector_swi를 통해 sys_clone() 함수로 전달됩니다.

 

linux# vim kernel/fork.c

 

 

 

위에서 확인한 내용을 요약하자면,

 - 유저공간에서 fork() 함수를 호출하면 r7레지스터에 시스템 콜 번호인 120 저장

 - svc 0x00000000 명령어 실행

 - 첫 번째 인자로 매크로 값 저장, 0x01200011

 

 

 커널을 좀 더 깊게 알기 위해서는 어셈블리어(Assembly Language)에 대한 학습도 필요하다고 생각됩니다.

 

 

감사합니다.

 

 

<참고 자료>

1. [도서] 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 p259 ~269 , wikibook

 

Makefile
0.00MB
raspberry_fork_test.c
0.00MB

 

반응형