$ 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 함수를 검색한다.
(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 스크립트를 사용한다.
(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를 실행하고 부팅 이후 애플리케이션을 실행해야 하며, 애플리케이션을 다시 컴파일 할 경우 이 과정을 매번 수행해야 한다.
#!/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 옵션을 사용할 수 있다.
#!/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 - step
n - next
fin - step out
i loc - info about local variables
i arg - info about function arguments
p <var> - print value of <var>
b <number> - set breakpoint at line <number> in the current file
b <file>:<number> - set breakpoint at line <number> in the <file> file
b <func> - break on function <func>
i b - info breakpoints
d <number> - delete breakpoint <number>
c - continue
q - quit GDB
Ctrl+p - previous GDB command in history
Ctrl+n - next GDB command in history
Ctrl+x and o - 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 and up/down/left/right - switch between panes
Ctrl+b and :kill-session - close all panes
Ctrl+b and Ctrl+up/down/left/right - change pane size