42. GDB를 이용한 Driver/Application 및 OVMF Debug
GDB를 사용하여 디버깅을 수행하기 위해서는 올바른 오프셋에 디버그 심볼을 로그해야 한다.
메모리 상의 애플리케이션 오프셋
애플리케이션 내부의 오프셋
이번 과정에서는 이전에 개발한 ShowBootVariable.efi
애플리케이션을 디버깅한다.
메모리 상의 애플리케이션 오프셋 구하기
QEMU에서 OVMF를 실행한다.
qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd \
-drive format=raw,file=fat:rw:~/UEFI_disk \
-net none \
-nographic \
-global isa-debugcon.iobase=0x402 \
-debugcon file:debug.log
또 다른 터미널을 이용하여 아래 명령어를 실행한다.
tail -f debug.log
이를 통해 런타임 로그를 추적할 수 있다.
부팅 이후 ShowBootVariables.efi
애플리케이션을 실행하고 아래와 같은 메세지를 debug.log
에서 확인한다.
Loading driver at 0x00006649000 EntryPoint=0x0000664C80F ShowBootVariables.efi
InstallProtocolInterface: BC62157E-3E33-4FEC-9920-2D3B36D750DF 666B898
ProtectUefiImageCommon - 0x666B4C0
- 0x0000000006649000 - 0x0000000000005540
InstallProtocolInterface: 752F3136-4E16-4FDC-A22A-E5F46812F4CA 7EA36C8
다음 문장에서 제작한 이미지가 로드된 주소를 확인한다. 0x6649000
Loading driver at 0x00006649000 EntryPoint=0x0000664C80F ShowBootVariables.efi
Application section의 offset 구하기
objdump
명령어를 이용하여 .efi
파일에 포함된 섹션을 확인한다.
$ objdump -h Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.efi
Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.efi: file format pei-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00004d40 0000000000000240 0000000000000240 00000240 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000580 0000000000004f80 0000000000004f80 00004f80 2**4
CONTENTS, ALLOC, LOAD, DATA
2 .reloc 00000080 0000000000005500 0000000000005500 00005500 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
또한 .debug
파일에 존재하는 섹션도 확인한다.
$ objdump -h Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.debug
Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.debug: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00004d15 0000000000000240 0000000000000240 000000c0 2**6
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 000004a1 0000000000004f80 0000000000004f80 00004e00 2**6
CONTENTS, ALLOC, LOAD, RELOC, DATA
2 .eh_frame 00000000 0000000000005440 0000000000005440 000052c0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .rela 00000510 0000000000005440 0000000000005440 000052c0 2**3
CONTENTS, READONLY
4 .build-id 00000024 0000000000005950 0000000000005950 000057d0 2**2
CONTENTS, READONLY
5 .debug_info 00026067 0000000000000000 0000000000000000 000057f4 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
6 .debug_abbrev 00003553 0000000000000000 0000000000000000 0002b85b 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
7 .debug_loc 00009fde 0000000000000000 0000000000000000 0002edae 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
8 .debug_aranges 00000570 0000000000000000 0000000000000000 00038d8c 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
9 .debug_ranges 000013f0 0000000000000000 0000000000000000 000392fc 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
10 .debug_line 00008163 0000000000000000 0000000000000000 0003a6ec 2**0
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
11 .debug_str 000065d7 0000000000000000 0000000000000000 0004284f 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
12 .debug_frame 000015a8 0000000000000000 0000000000000000 00048e28 2**3
CONTENTS, RELOC, READONLY, DEBUGGING, OCTETS
두 정보의 차이는 .debug
정보의 유무이다.
objdump
결과를 통해서 필요한 섹션의 오프셋을 확인할 수 있다. (환경에 따라 그 결과가 다를 수도 있다.)
.text
->0x240
.data
->0x4f40
GDB에 디버그 심볼적용
QEMU 실행 시 -s
옵션을 포함하여 원격 디버깅을 활성화 한다. 해당 옵션은 tcp::1234
의 축어이며 QEMU는 해당 포트에서 GDB 연결을 수신할 수 있다.
qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd \
-drive format=raw,file=fat:rw:~/UEFI_disk \
-net none \
-nographic \
-s
심볼 적용을 위해 사전에 확인한 애플리케이션 이미지 주소에 구한 .text
와 .data
의 오프셋 값 연산한다.
0x6649000 + 0x240 = 0x6649240
0x6649000 + 0x4f40 = 0x664df40
GDB를 실행하고 다음과 같이 심볼을 적용한다.
$ gdb
(gdb) add-symbol-file Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.debug 0x65FD240 -s .data 0x6601F80
add symbol table from file "Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.debug" at
.text_addr = 0x65fd240
.data_addr = 0x6601f80
(y or n) y
Reading symbols from Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.debug...
애플리케이션의 Entry Point에 break point을 적용하기 위해서 ShellAppMain
함수를 검색한다.
$ grep -n ShellAppMain UefiLessonsPkg/ShowBootVariables/ShowBootVariables.c
63:INTN EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)
이후 진입점에 break point를 설정한다.
(gdb) b UefiLessonsPkg/ShowBootVariables/ShowBootVariables.c:63
Breakpoint 1 at 0x664c50f: file /home/kostr/tiano/edk2/UefiLessonsPkg/ShowBootVariables/ShowBootVariables.c, line 68.
원격 디버깅을 실행한다.
(gdb) target remote :1234
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x000000000716e151 in ?? ()
연결 이후 QEMU는 중단되며 재실행을 위해서는 GDB에서 c
명령어를 입력하면 된다.
(gdb) c
Continuing.
설정한 break point를 trigger 하기 위해 애플리케이션을 실행한다.
FS0:\> ShowBootVariables.efi
Breakpoint 1, ShellAppMain (Argc=1, Argv=0x6609b98)
at /home/minishell/src/edk2-ws/edk2/UefiLessonsPkg/ShowBootVariables/ShowBootVariables.c:69
69 Status = GetNvramVariable(L"BootCurrent", &gEfiGlobalVariableGuid, (VOID**)&BootCurrent, &OptionSize);

GDB에 편의를 위한 TUI 기능이 존재한다.
(gdb) tui enable

efi.py
앞선 과정들을 효율적으로 수행하기 위해서 Artem Nefedov 의 efi.py 스크립트를 사용한다.
해당 스크립트를 통해 디버그 심볼을 로드하는데 큰 도움을 받을 수 있다. https://github.com/artem-nefedov/uefi-gdb/blob/master/efi.py
$ gdb -ex 'source efi.py'
gdb 내부에서 추가된 efi
명령어를 이용해 디버그 심볼을 로드한다.
(gdb) efi -64 ShowBootVariables
Turning pagination off
Using pre-defined driver list: ['ShowBootVariables']
The target architecture is assumed to be i386:x86-64:intel
With architecture X64
Looking for addresses in debug.log
EFI file Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.efi
Base address 0x000065FD000
.text address 0x0000000000000240
.data address 0x0000000000004f80
add symbol table from file "Build/UefiLessonsPkg/RELEASE_GCC5/X64/ShowBootVariables.debug" at
.text_addr = 0x65fd240
.data_addr = 0x6601f80
Restoring pagination
위 과정은 스크립트가 ShowBootVariables
문자열에 대해 debug.log
파일에서 검색하고 주소 정보를 가져온 다음 필요한 심볼을 구해 gdb에 로드한다.
디버깅을 시도해보면 앞선 과정과 동일한 결과를 얻을 수 있다.
run_gdb.sh
efi.py
도 훌륭한 스크립트이지만 아직 수행할 작업이 많다.
적절한 인자로 QEMU를 실행하고 부팅 이후 애플리케이션을 실행해야 하며, 애플리케이션을 다시 컴파일 할 경우 이 과정을 매번 수행해야 한다.
efi.py
를 GDB에 로드한 뒤 실행하여efi
명령을 실행해야 한다.break point 지정을 위해 함수 Entry Point를 검색해야 한다.
QEMU 환경을 디버깅하기 위해 별도의 터미널이 요구된다.
GDB를 QEMU에 연결하고 애플리케이션을 실행해야 한다.
따라서 디버깅과 동시에 OVMF 로그를 볼 수 있으며 위 과정을 자동화할 수 있는 스크립트를 작성하자. https://github.com/Kostr/UEFI-Lessons/blob/master/scripts/run_gdb.sh
#!/bin/bash
##
# Copyright (c) 2021, Konstantin Aladyshev <aladyshev22@gmail.com>
#
# SPDX-License-Identifier: MIT
##
##### Controllable parameters #####
QEMU_SHARED_FOLDER=~/UEFI_disk
PACKAGE=UefiLessonsPkg
###################################
function show_help {
echo "Description:"
echo " run_gdb.sh is a script that helps to debug UEFI shell applications and drivers"
echo ""
echo "Usage: run_gdb.sh -m <module> [-1|-f|-p <package>|-q <dir>]"
echo " -1 This is a first run of this configuration"
echo " (in this case before main gdb launch there would be another QEMU start that will create 'debug.log' file)"
echo " -f Load all debug symbols"
echo " (this will load all OVMF debug symbols - with this you could step inside OVMF functions)"
echo " -m <module> UEFI module to debug"
echo " -p <package> UEFI package to debug"
echo " (by default it is equal to PACKAGE variable in the head of the script)"
echo " -q <dir> QEMU shared directory"
echo " (by default it is equal to QEMU_SHARED_FOLDER variable in the head of the script)"
echo ""
echo "Examples:"
echo " run_gdb.sh -1 -m MyApp - create 'debug.log' file with the necessary address information for the 'MyApp'"
echo " and debug it with gdb"
echo " run_gdb.sh -1 -m MyApp -f - create 'debug.log' file with the necessary address information for the 'MyApp'"
echo " and debug it with gdb (all debug symbols are included, i.e. you can step into OVMF functions)"
echo " run_gdb.sh -m MyApp - debug 'MyApp' with gdb ('debug.log' was created in the last run, no need to remake it again)"
}
# A POSIX variable
OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?1fm:q:p:" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
1) FIRST_RUN=1
;;
f) FULL=1
;;
m) TARGET=$OPTARG
;;
q) QEMU_SHARED_FOLDER=$OPTARG
;;
p) PACKAGE=$OPTARG
;;
esac
done
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
if [[ ! -z "${FULL}" ]]; then
DRIVERS=''
else
DRIVERS=${TARGET}
fi
if [[ -z $TARGET ]]; then
echo "Error! Module is not provided."
echo ""
show_help
exit 1
fi
function test_file {
FILE_NAME=$1
if [[ ! -f ${FILE_NAME} ]]; then
echo "Error! There is no file ${FILE_NAME}"
exit 1;
fi
}
TARGET_INF="${PACKAGE}/${TARGET}/${TARGET}.inf"
TARGET_C="${PACKAGE}/${TARGET}/${TARGET}.c"
OVMF="Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd"
test_file "${TARGET_INF}"
test_file "${TARGET_C}"
test_file "${OVMF}"
ENTRY_POINT_NAME=$(grep ENTRY_POINT ${TARGET_INF} | cut -f 2 -d "=")
if [ ${ENTRY_POINT_NAME} == "ShellCEntryLib" ]; then
ENTRY_POINT_NAME="ShellAppMain"
fi
ENTRY_POINT_LINE=$(grep -n ${ENTRY_POINT_NAME} ${TARGET_C} | cut -f 1 -d ":")
MODULE_TYPE=$(grep MODULE_TYPE ${TARGET_INF} | cut -f 2 -d "=")
if [ ${MODULE_TYPE} == "UEFI_DRIVER" ]; then
LAUNCH_COMMAND="load fs0:${TARGET}.efi"
else
LAUNCH_COMMAND="fs0:${TARGET}.efi"
fi
if [[ ! -z "${FIRST_RUN}" || ! -f debug.log ]]; then
touch debug.log
# If it is a first run, we need to create 'debug.log' file for addresses
TARGET_EFI="Build/${PACKAGE}/RELEASE_GCC5/X64/${TARGET}.efi"
test_file "${TARGET_EFI}"
cp ${TARGET_EFI} ${QEMU_SHARED_FOLDER}
tmux new-session \; \
send-keys "tail -f debug.log" Enter \; \
split-window -v \; \
send-keys "qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=${OVMF} \
-drive format=raw,file=fat:rw:${QEMU_SHARED_FOLDER} \
-net none \
-nographic \
-global isa-debugcon.iobase=0x402 \
-debugcon file:debug.log \
-s" C-m Enter \; \
send-keys C-m Enter \; \
send-keys "${LAUNCH_COMMAND}" Enter \;
fi
test_file "${QEMU_SHARED_FOLDER}/${TARGET}.efi"
touch debug_temp.log
tmux new-session \; \
send-keys "gdb -ex 'source efi.py' -tui" Enter \; \
split-window -h \; \
send-keys "tail -f debug_temp.log" Enter \; \
split-window -v \; \
send-keys "qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=${OVMF} \
-drive format=raw,file=fat:rw:${QEMU_SHARED_FOLDER} \
-net none \
-nographic \
-global isa-debugcon.iobase=0x402 \
-debugcon file:debug_temp.log \
-s" C-m Enter \; \
select-pane -t 0 \; \
send-keys "efi -64 ${DRIVERS}" Enter \; \
send-keys "b ${TARGET_C}:${ENTRY_POINT_LINE}" Enter \; \
send-keys Enter \; \
send-keys "target remote :1234" Enter \; \
send-keys "c" Enter \; \
select-pane -t 2 \; \
send-keys C-m Enter \; \
send-keys "${LAUNCH_COMMAND}" Enter \; wnd
위 코드는 tmux
세션을 이용하여 필요한 키를 자동적으로 전달할 수 있다.
$ ./run_gdb.sh -h
Description:
run_gdb.sh is a script that helps to debug UEFI shell applications and drivers
Usage: run_gdb.sh -m <module> [-1|-f|-p <package>|-q <dir>]
-1 This is a first run of this configuration
(in this case before main gdb launch there would be another QEMU start that will create 'debug.log' file)
-f Load all debug symbols
(this will load all OVMF debug symbols - with this you could step inside OVMF functions)
-m <module> UEFI module to debug
-p <package> UEFI package to debug
(by default it is equal to PACKAGE variable in the head of the script)
-q <dir> QEMU shared directory
(by default it is equal to QEMU_SHARED_FOLDER variable in the head of the script)
Examples:
run_gdb.sh -1 -m MyApp - create 'debug.log' file with the necessary address information for the 'MyApp'
and debug it with gdb
run_gdb.sh -1 -m MyApp -f - create 'debug.log' file with the necessary address information for the 'MyApp'
and debug it with gdb (all debug symbols are included, i.e. you can step into OVMF functions)
run_gdb.sh -m MyApp - debug 'MyApp' with gdb ('debug.log' was created in the last run, no need to remake it again)
처음 디버깅하는 경우 debug.log
를 생성해야 한다.
./run_gdb.sh -1 -m ShowBootVariables
이후 QEMU 환경에서 enter
만 입력해도 대상 애플리케이션이 실행된다.

이후 다른 터미널을 통해 tmux 세션을 닫거나 tmux 환경에서 ctrl+b
입력하고 :kill-session
을 입력하여 tmux 세션을 닫는다.

다시 로드된 QEMU 환경과 디버그 심볼이 추가된 GDB 환경을 확인할 수 있다. QEMU 환경에서 enter
를 입력하면 대상 애플리케이션 Entry Point에break point가 발생한다.

모든 symbols 로드하기
만약 애플리케이션 또는 드라이버 외부에서 정의된 함수 (예: gRT->GetVariable)를 단계별로 실행하려면 모든 심볼을 GDB에 로드해야 한다.
이 때 -f
인자를 통해 run_gdb.sh
를 실행하면 된다.
./run_gdb.sh -1 -m ShowBootVariables -f
함수들의 심볼이 존재하므로 원하는 지점에 break point를 설정할 수 있다.
OVMF 자체 Debug
OVMF 자체를 디버깅할 경우에 시작 시 QEMU를 중지해야 한다. 이 작업을 위해 -S
옵션을 사용할 수 있다.
qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd \
-drive format=raw,file=fat:rw:~/UEFI_disk \
-net none \
-nographic \
-s \
-S
이후 GDB을 실행하고 이전과 같이 debug.log
파일을 통해 필요 symbols를 로드해야 한다.
아래는 해당 작업을 자동화하기 위한 run_gdb_ovmf.sh
스크립트이다.
https://github.com/Kostr/UEFI-Lessons/blob/master/scripts/run_gdb_ovmf.sh
#!/bin/bash
##
# Copyright (c) 2021, Konstantin Aladyshev <aladyshev22@gmail.com>
#
# SPDX-License-Identifier: MIT
##
##### Controllable parameters #####
QEMU_SHARED_FOLDER=~/UEFI_disk
###################################
function show_help {
echo "Description:"
echo " run_gdb_ovmf.sh is a script that helps to debug OVMF"
echo ""
echo "Usage: run_gdb_ovmf.sh [-1] [-q <dir>]"
echo " -1 This is a first run of this configuration"
echo " (in this case before main gdb launch there would be another QEMU start that will create 'debug.log' file)"
echo " -q <dir> QEMU shared directory"
echo " (by default it is equal to QEMU_SHARED_FOLDER variable in the head of the script)"
echo ""
echo "Examples:"
echo " run_gdb_ovmf.sh -1 - create 'debug.log' file with the necessary address information"
echo " and debug OVMF it with gdb"
echo " run_gdb_ovmf.sh - debug OVMF with gdb ('debug.log' was created in the last run, no need to remake it again)"
}
# A POSIX variable
OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?1q:" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
1) FIRST_RUN=1
;;
q) QEMU_SHARED_FOLDER=$OPTARG
;;
esac
done
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
function test_file {
FILE_NAME=$1
if [[ ! -f ${FILE_NAME} ]]; then
echo "Error! There is no file ${FILE_NAME}"
exit 1;
fi
}
OVMF="Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd"
test_file "${OVMF}"
if [[ ! -z "${FIRST_RUN}" || ! -f debug.log ]]; then
touch debug.log
# If it is a first run, we need to create 'debug.log' file for addresses
tmux new-session \; \
send-keys "tail -f debug.log" Enter \; \
split-window -v \; \
send-keys "qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=${OVMF} \
-drive format=raw,file=fat:rw:${QEMU_SHARED_FOLDER} \
-net none \
-nographic \
-global isa-debugcon.iobase=0x402 \
-debugcon file:debug.log \
-s" C-m Enter \;
fi
touch debug_temp.log
tmux new-session \; \
send-keys "gdb -ex 'source efi.py' -tui" Enter \; \
split-window -h \; \
send-keys "tail -f debug_temp.log" Enter \; \
split-window -v \; \
send-keys "qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=${OVMF} \
-drive format=raw,file=fat:rw:${QEMU_SHARED_FOLDER} \
-net none \
-nographic \
-global isa-debugcon.iobase=0x402 \
-debugcon file:debug_temp.log \
-s -S" C-m Enter \; \
select-pane -t 0 \; \
send-keys "efi -64" Enter \; \
send-keys "target remote :1234" Enter \;
$ ./run_gdb_ovmf.sh -h
Description:
run_gdb_ovmf.sh is a script that helps to debug OVMF
Usage: run_gdb_ovmf.sh [-1] [-q <dir>]
-1 This is a first run of this configuration
(in this case before main gdb launch there would be another QEMU start that will create 'debug.log' file)
-q <dir> QEMU shared directory
(by default it is equal to QEMU_SHARED_FOLDER variable in the head of the script)
Examples:
run_gdb_ovmf.sh -1 - create 'debug.log' file with the necessary address information
and debug OVMF it with gdb
run_gdb_ovmf.sh - debug OVMF with gdb ('debug.log' was created in the last run, no need to remake it again)
run_gdb.sh
와 별반 다르지 않지만 break point을 설정하거나 OVMF를 실행하지 않는다는 차이점이 존재한다.

GDB cheatsheet
아래는 GDB 환경에서 사용할 수 있는 명령어 일부의 정보이다.
s
- stepn
- nextfin
- step outi loc
- info about local variablesi arg
- info about function argumentsp <var>
- print value of<var>
b <number>
- set breakpoint at line<number>
in the current fileb <file>:<number>
- set breakpoint at line<number>
in the<file>
fileb <func>
- break on function<func>
i b
- info breakpointsd <number>
- delete breakpoint<number>
c
- continueq
- quit GDBCtrl+p
- previous GDB command in historyCtrl+n
- next GDB command in historyCtrl+x
ando
- change active window in tui mode
16 진수로 값을 출력하고 싶을 경우에는 p/x
를 사용한다.
CHAR16 문자열을 출력할 경우 x /sh
명령어를 이용할 수 있다. 다음은 장치 경로를 출력하는 예제이다.
(gdb) p ConvertDevicePathToText(DevicePath, 0, 1)
$1 = (CHAR16 *) 0x6d04518
(gdb) x /sh 0x6d04518
0x6d04518: u"PciRoot(0x0)/Pci(0x2,0x0)"
또는
(gdb) x /sh ConvertDevicePathToText(DevicePath, 0, 1)
0x6d02098: u"PciRoot(0x0)/Pci(0x2,0x0)"
만약 출력 데이터의 길이가 길어 완전히 출력하지 못할 경우 다음 명령어를 통해 출력 길이 제한을 해제할 수 있다.
(gdb) set print elements 0
TMUX cheatsheet
Ctrl+b
andup/down/left/right
- switch between panesCtrl+b
and:kill-session
- close all panesCtrl+b
andCtrl+up/down/left/right
- change pane size
Last updated