ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [개발 도구] GDB
    Linux/development tool 2023. 8. 7. 01:57

    글의 참고

    http://egloos.zum.com/psyoblade/v/2653919

    https://stackoverflow.com/questions/2956889/how-to-make-a-gdb-breakpoint-only-break-after-the-point-is-reached-a-given-numbe

    https://stackoverflow.com/questions/4183871/how-do-i-set-a-conditional-breakpoint-in-gdb-when-char-x-points-to-a-string-wh

    - https://man7.org/linux/man-pages/man1/gdb.1.html


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.

    - `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.

    - `운영체제 만들기` 파트에서 퍼온 모든 참조 글들과 그림은 반드시 `이 글과 그림을 소스 코드로 어떻게 구현을 해야할까` 라는 생각으로 정말 심도있게 잠시 멈춰서 생각해봐야 실력이 발전한다.


    글의 내용

    : Custom OS 개발 시, GDB는 필수다.

    - 커맨드 라인

    : 브레이크 포인트 확인

    - info b

     

    : 브레이크 포인트 걸기

    : b mm.c:589   ==> mm.c의 파일에 589 라인에 break point.

     

    : 조건부 브레이크 포인트 걸기

    : b mm.c:589 if i == 3     ==> mm.c 파일에 589 라인에 i == 3 일 때, 멈춤.

     

    : 브레이크 포인트 삭제

    - delete [브레이크 포인트 번호]

     

    : 특정 주소의 값이 바뀔 때, 브레이크 걸기

    - watch *0x2030222f

     

    : 콜 스택 확인

    - bt

     

    : 현재 라인 확인

    - frame(f)

     

    : x/x 0x1fed03e2

    " 주소 0x1fed03e2의 값을 16 진수로 보여줌 

     

    - set architecture <arg>

    " GDB에서 디버깅할 바이너리의 아키텍처를 설정한다.

     

    - file <symbol>

    " 디버깅 심볼을 읽어들인다.

     

    - info frame

    " 현재 브레이크 포인트에 멈춰있는 함수의 스택 프레임을 보여준다.

     

    - x/<n>x *0x3222ef

    " 0x3222ef를 기준으로 <n>바이트 만큼의 메모리 상태를 보여준다.

     

    : x/10i $pc

    " 그 다음 실행될 10개의 명령어를 보여준다. 숫자에 따라 보여줄 명령어가 달라진다.

     

     

    : layout src & regs

    " GDB를 이쁘게 볼 수 있게 해준다. 이제 현재 어디 라인이 실행되는지를 보기 위해 `f`를 입력한다거나 하는 짓은 할 필요가 없다.

     

    - 옵션

    : gdb <file>

    " <file> 에 심볼이 명시될 경우, 커맨드 라인에서 `file` 명령어로 심볼을 읽어들일 필요가 없다.

     

     

     

    - GDB로 QEMU x86 아키텍처 디버깅

    : 먼저 결론은 간단하다. 16비트 및 32비트를 하나로 묶어서 디버깅이 가능하고, 64비트는 별도로 디버깅 해야 한다. 그 이유는 GDB가 i386과 x86_64를 동시에 지원하지 못하기 때문이다. QEMU는 상관이 없다. 왜냐면, 16비트, 32비트, 64비트 코드로 작성된 바이너리 전부 `qemu-system-x86_64`에서 잘 동자하기 때문이다. 문제는 GDB다. GDB는 상당히 아키텍처에 의존적인 디버거다. 그래서 GDB는 디버깅시에 타겟 아키텍처를 따라서 동작하게 된다. `set architecture` 명령어를 통해서 GDB에서 사용 가능한 아키텍처들을 보여준다. 그리고 `show architecture`를 통해 GDB가 현재 가정하고 있는 타겟 아키텍처를 보여준다.

     

    : 그리고 `show configuration`을 입력하면 아래의 정보를 볼 수 있다. 진짜 문제는 아래의 `--target` 이지 않을까 싶은 생각이든다. GDB의 타겟이 x86_64로 고정이 되버려서, i386을 인식하지 못하는 것처럼 보인다.

    GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
    Copyright (C) 2018 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "x86_64-linux-gnu".
    Type "show configuration" for configuration details.
    For bug reporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>.
    Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.
    For help, type "help".
    Type "apropos word" to search for commands related to "word".
    (gdb) show configuration 
    This GDB was configured as follows:
       configure --host=x86_64-linux-gnu --target=x86_64-linux-gnu
                 --with-auto-load-dir=$debugdir:$datadir/auto-load
                 --with-auto-load-safe-path=$debugdir:$datadir/auto-load
                 --with-expat
                 --with-gdb-datadir=/usr/share/gdb (relocatable)
                 --with-jit-reader-dir=/usr/lib/gdb (relocatable)
                 --without-libunwind-ia64
                 --with-lzma
                 --with-python=/usr (relocatable)
                 --without-guile
                 --with-separate-debug-dir=/usr/lib/debug (relocatable)
                 --with-system-gdbinit=/etc/gdb/gdbinit
                 --with-babeltrace

    ("Relocatable" means the directory can be moved with the GDB installation
    tree, and GDB will still find it.)
    (gdb)

     

    : 참고로 아래는 GDB 8.1.1 `x86_64-linux-gnu` 크로스 컴파일 기준으로 빌드된 GDB의 아키텍처 지원 정보이다.

    i386, i386:x86-64, i386:x64-32, i8086, i386:intel, i386:x86-64:intel, i386:x64-32:intel, i386:nacl, i386:x86-64:nacl, i386:x64-32:nacl, auto.

    : 위에서 보여주는 아키텍처들은 각자 서로 호환이 안된다고 보면 된다. GDB는 마치 QEMU 에뮬레이터가 달라져서 자신이 디버깅하는 코드도 달라져야 한다는 것처럼 에러를 뿜지만, 실제로 GDB를 사용하지 않고 QEMU만으로 16비트, 32비트, 64비트 바이너리를 돌리만 아주 잘 동작한다. 결국 GDB를 32비트용, 64비트용으로 나눠야 될 듯 싶다. 여기서 말한 에러는 이 에러와 관련이 있다. -> 확인 결과 32비트 i686-elf-gdb를 qemu-system-x86_64에서 사용하면 위에 링크에 걸린 에러와 비슷한 것이 발생한다. 그리고 링크를 따라가 보면 알게지만, GDB가 문제는 맞다. 그러나, 수정을 할 수 없는 부분인 듯 하다. 그래서 사람들은 BOCHS를 사용하거나 16비트 및 32비트는 qemu-system-i386, 64비트는 qemu-systme-x86_64를 사용하는 듯 하다. 

     

    : 나는 BOCHS를 추천한다. 범용성을 위해서는 GDB가 맞겠지만, qemu-system-i386에서 CPUID 확장 기능 0x8000_0001을 읽으면 64비트를 지원하지 않는다고 출력한다. 그래서 64비트로 진입하기 전부터 문제가 좀 있으니 BOCHS에 사용을 고려해보길 바란다.

     

     

    - GDB를 통해 x86 모드 변경 시, 매끄럽게 디버깅

    : 16비트에서 32비트로 가고, 32비트에서 64비트로 갈 때, 디버깅이 매끄럽게 이어지기 위해서는 점프를 크게하는 부분에서 파일을 바꿔줘야 한다. 사실 GDB는 x86의 모드 체인지를 인식하는 기능이 없어서 없어서 처음 실행할 때, 디버깅할 타겟의 아키텍처를 알려줘야 한다.

     

    : 아래의 코드를 보면 각 명령어들의 주소를 붉은색으로 표시했다. GDB를 `qemu-system-x86_64` 로 타겟을 잡을 경우, 기본적으로 `R` 접두사 레지스터들을 사용한다. 크로스 컴파일러는 `x86_64-elf`를 사용했다. 이제 아래의 코드에서 첫 번째 점프를 보면, FAR JMP로 GDT를 64비트로 바꾸면서 64비트 아래 주소 영역으로 점프한다. 

     

    : 그리고 `_trampoline64`에서 실제 64비트 상위 영역으로 점프를 하게 된다. 그런데, 여기서 GDB가 `No Available Soruce` 를 뿌리면서 코드를 찾지 못한다. 내 추측이지만, 그 이유는 해당 파일이 32비트 코드로 작성되어 있기 때문이라 생각된다. 아래 파일에 맨 위를 보면 `[bits 32]` 는 NASM이 해당 파일을 32비트로 어셈블하게 한다. 그러다가, 아래 `[bits 64]`를 만나면 그 아래부터는 64비트로 어셈블하게 한다. 그런데, GDB는 이 사실을 모른다. GDB는 타겟이 qemu-system-x86_64 라는 것만 안다. 그래서 GDB는 낮은 주소 영역(4GB 이하)에 대해 디버깅을 하다가, 동일 파일내에서 높은 주소 영역(4GB 이상)으로 점프를 하면 소스를 제대로 인식하지 못하는 것으로 보인다. 이 문제는 16비트에서 32비트로 1MB 를 기준으로 점프 할 때도 동일하다. 

    [bits 32]
    ...
    ...
    ; Re-enable Long mode
        mov eax, cr0
        or eax, 1<<31
        mov cr0, eax

        jmp 0x18:_trampoline64  ; 0x10008a 

    [bits 64]
    extern _lenter
    _trampoline64:  ; 0x10008c
        jmp _lenter

    section .text
    _lenter:  ; 0xffffffff80106000
        mov dword [page_low_pd], 0
        mov dword [page_low_pd+8], 0 
        mov dword [page_low_pdtp], 0
        mov dword [page_pml4], 0
            
        invlpg [0]
            
        mov rax, 0x20

        mov ds, rax
        mov es, rax
        mov fs, rax
        mov gs, rax
            
        mov ss, rax
        mov rbp, 0x300000 ; 32bit end point
        mov rsp, 0x300000 ; 32bit end point

        call main

    : 그럼 이제 아래와 같이 파일을 분리해보자.

     

    ; lbl.asm
    [bits 32]

    ...
    ...
    ; Re-enable Long mode
        mov eax, cr0
        or eax, 1<<31
        mov cr0, eax

        jmp 0x18:_trampoline64  ; 0x10008a 

    [bits 64]
    extern _lenter
    _trampoline64:  ; 0x10008c
        jmp _lenter


    ; mode64.asm
    [bits 64]

    section .text

    global _lenter
    _lenter:  ; 0xffffffff80106000

        mov dword [page_low_pd], 0
        mov dword [page_low_pd+8], 0 
        mov dword [page_low_pdtp], 0
        mov dword [page_pml4], 0
            
        invlpg [0]
            
        mov rax, 0x20

        mov ds, rax
        mov es, rax
        mov fs, rax
        mov gs, rax
            
        mov ss, rax
        mov rbp, 0x300000 ; 32bit end point
        mov rsp, 0x300000 ; 32bit end point

        call main

    : 위와 같이 소스를 분리하면, GDB가 64비트로 넘어갔다는 것을 정확히 인식해서 소스가 보이기 시작한다.

     

     

    - BIOS 인터럽트

    : 기본 설정은 이 글을 참고하자. 여기서는 몇 가지 팁을 제공하려 한다. 16비트 x86는 BIOS의 기능에 상당히 의존한다. 그런데, BIOS의 소스 레벨에서 디버깅이 안된다. 그래서 GDB에서 BIOS INT를 만나면 이상한 주소로 이동해서 소스를 찾을 수 없다는 내용을 만나게 된다. BIOS INT는 `n` 키를 눌러도 STEP OVER이 되지 않는다. 그렇면 어떻게 넘어갈까?

     

    : 가장 간단한 방법은 BIOS INT 다음 라인 넘버에 브레이크 포인트를 잡고 `c`를 눌러, 넘어가는 방법이 가장 쉽다. 아래의 코드를 보자.

    ...
    mov al, 1                       ; read one sector
    mov ah, BIOS_READ_SECS          ; BIOS INT 13h F2h:Read Sectors from drive
    mov ch, byte [start_cylin]      ; start to cylinder
    mov cl, byte [start_sec]        ; start to sector
    mov dh, byte [start_head]       ; start to head
    mov dl, 0                       ; set disk drive to 0
    
    int 0x13                        ; request BIOS INT13h F2h
    jc _error                       ; If carry set, error                                        
                
    cmp al, 1                       ; If successed to read, return the read sectors count to al
    jne _error
    ...

    : `int 0x13`을 넘어가려면 뒤쪽에 아무데나 브레이크 포인트를 잡고 `c`키로 넘어가야 한다. 

     

     

     

    - GDB 한계

    : GDB로는 16비트 부트 로더를 디버깅에 제약이 있다. 일단 결론만 말하면 x86기반의 16비트 부트 로더 디버깅이 가능하다. 그러나, 세그먼트 주소 지정 방식을 사용해서 디버깅을 하기는 어렵다. GDB는 [R|E]IP를 기반으로만 동작한다. CS는 무시한다. 그러나, 실제 동작은 CS:IP 형태로 코드 흐름이 동작한다. 그래서 QEMU를 GDB로 디버깅 하고 싶다면, 세그먼트 레지스터의 값을 모두 0으로 세팅해야 한다.

     

    어떤 사람들은 GDB에 `set architecture i8086` 설정을 하면 된다고 하지만, 확인 결과 GDB는 [R|E]IP를 기반으로만 동작한다. 못 믿겠다면, GDB에서 `layout regs` 기능을 키고 CS에 0이 아닌 값을 쓰고 코드의 흐름과 `x/10i $pc`에 어떤 값들이 나오는지 확인해보자.

     

    : GDB로 디버깅한다면 무엇이 됬든 16비트 환경에서 CS는 0으로 설정해야 한다는 것이다. 이 말은 FAR JUMP도 하면 안된다는 말이다. 왜냐면, FAT JUMP는 CS를 설정하기 때문이다. CS를 설정하면 코드 중간중간 건너뛰고 디버깅을 하게되고 값도 이상한 값들이 써진다. 그래서 x86 16비트는 FLAT MODEL 기반으로 코드를 작성하는게 GDB 디버깅을 하는데 좋다. 그러나, 세그먼트 주소 지정 방식을 이해하고 있는 좋은 디버거가 있다. 동시에 에뮬레이터 기능도 하는 프로그램이 있다. 바로 `BOSCH`다.

     

    : 사실, x86 개발에는 BOSCH를 사용하는게 가장 좋다. BOSCH 에뮬레이터는 x86 아키텍처 전용으로 만들어진 에뮬레이터다. 그래서 x86만 지원한다. 그러나, Bosch는 환경 설정 및 구성이 어렵다는 점과 다른 아키텍처들을 지원하지 않는다는 단점이 존재한다.

     

    : gdb-multiarch가 아키텍처 관련 더 많은 기능이 추가됬다고 해서 사용해봤지만, 여전히 세그먼트 주소 지정 방식을 이해하지 못하고 있는듯 하다.

    'Linux > development tool' 카테고리의 다른 글

    [GIT] 리눅스 커널 코드 분석  (0) 2023.10.05
    [개발 도구] QEMU  (4) 2023.08.07
    [개발 도구] linker script  (0) 2023.08.03
    [개발 도구] Shell Prompt 언어 설정  (0) 2023.08.03
    [LINUX][VIM] - Vim Session  (0) 2023.08.03
Designed by Tistory.