feat(debugging): move gdbinit generation to CMake

This feature is useful for 3rd-party software to run GDB with predefined
options that described in project_description.json file

allow to pass custom options to "idf.py gdb":

  --gdb-commands: command line arguments for gdb. (without changes)
  -ex: pass command to gdb.
  -x: pass gdbinit file to gdb. Alias for old --gdbinit command
This commit is contained in:
Alexey Lapshin
2024-11-06 17:01:33 +07:00
parent 15c32509dd
commit 080f1a0b0d
17 changed files with 394 additions and 316 deletions

View File

@@ -4,12 +4,10 @@ import json
import os
import re
import shlex
import shutil
import subprocess
import sys
import threading
import time
from textwrap import indent
from threading import Thread
from typing import Any
from typing import Dict
@@ -21,8 +19,6 @@ from typing import Union
from click import INT
from click.core import Context
from esp_coredump import CoreDump
from idf_py_actions.constants import OPENOCD_TAGET_CONFIG
from idf_py_actions.constants import OPENOCD_TAGET_CONFIG_DEFAULT
from idf_py_actions.errors import FatalError
from idf_py_actions.serial_ext import BAUD_RATE
from idf_py_actions.serial_ext import PORT
@@ -33,53 +29,6 @@ from idf_py_actions.tools import get_sdkconfig_value
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import yellow_print
PYTHON = sys.executable
ESP_ROM_INFO_FILE = 'roms.json'
GDBINIT_PYTHON_TEMPLATE = '''
# Add Python GDB extensions
python
import sys
sys.path = {sys_path}
import freertos_gdb
end
'''
GDBINIT_PYTHON_NOT_SUPPORTED = '''
# Python scripting is not supported in this copy of GDB.
# Please make sure that your Python distribution contains Python shared library.
'''
GDBINIT_BOOTLOADER_ADD_SYMBOLS = '''
# Load bootloader symbols
set confirm off
add-symbol-file {boot_elf}
set confirm on
'''
GDBINIT_BOOTLOADER_NOT_FOUND = '''
# Bootloader elf was not found
'''
GDBINIT_APP_ADD_SYMBOLS = '''
# Load application file
file {app_elf}
'''
GDBINIT_CONNECT = '''
# Connect to the default openocd-esp port and break on app_main()
set remotetimeout 10
target remote :3333
monitor reset halt
maintenance flush register-cache
thbreak app_main
continue
'''
GDBINIT_MAIN = '''
source {py_extensions}
source {symbols}
source {connect}
'''
def get_openocd_arguments(target: str) -> str:
default_args = OPENOCD_TAGET_CONFIG_DEFAULT.format(target=target)
return str(OPENOCD_TAGET_CONFIG.get(target, default_args))
def chip_rev_to_int(chip_rev: Optional[str]) -> Union[int, None]:
# The chip rev will be derived from the elf file if none are returned.
@@ -223,103 +172,6 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
return subprocess.run([gdb, '--batch-silent', '--ex', 'python import os'],
stderr=subprocess.DEVNULL).returncode == 0
def get_normalized_path(path: str) -> str:
if os.name == 'nt':
return os.path.normpath(path).replace('\\', '\\\\')
return path
def get_rom_if_condition_str(date_addr: int, date_str: str) -> str:
r = []
for i in range(0, len(date_str), 4):
value = hex(int.from_bytes(bytes(date_str[i:i + 4], 'utf-8'), 'little'))
r.append(f'(*(int*) {hex(date_addr + i)}) == {value}')
return 'if ' + ' && '.join(r)
def generate_gdbinit_rom_add_symbols(target: str) -> str:
base_ident = ' '
rom_elfs_dir = os.getenv('ESP_ROM_ELF_DIR')
if not rom_elfs_dir:
raise FatalError(
'ESP_ROM_ELF_DIR environment variable is not defined. Please try to run IDF "install" and "export" scripts.')
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), ESP_ROM_INFO_FILE), 'r') as f:
roms = json.load(f)
if target not in roms:
msg_body = f'Target "{target}" was not found in "{ESP_ROM_INFO_FILE}". Please check IDF integrity.' # noqa: E713
if os.getenv('ESP_IDF_GDB_TESTING'):
raise FatalError(msg_body)
print(f'Warning: {msg_body}')
return f'# {msg_body}'
r = ['', f'# Load {target} ROM ELF symbols']
r.append('define target hookpost-remote')
r.append('set confirm off')
# Since GDB does not have 'else if' statement than we use nested 'if..else' instead.
for i, k in enumerate(roms[target], 1):
indent_str = base_ident * i
rom_file = get_normalized_path(os.path.join(rom_elfs_dir, f'{target}_rev{k["rev"]}_rom.elf'))
build_date_addr = int(k['build_date_str_addr'], base=16)
r.append(indent(f'# if $_streq((char *) {hex(build_date_addr)}, "{k["build_date_str"]}")', indent_str))
r.append(indent(get_rom_if_condition_str(build_date_addr, k['build_date_str']), indent_str))
r.append(indent(f'add-symbol-file {rom_file}', indent_str + base_ident))
r.append(indent('else', indent_str))
if i == len(roms[target]):
# In case no one known ROM ELF fits - print error and exit with error code 1
indent_str += base_ident
msg_body = f'unknown {target} ROM revision.'
if os.getenv('ESP_IDF_GDB_TESTING'):
r.append(indent(f'echo Error: {msg_body}\\n', indent_str))
r.append(indent('quit 1', indent_str))
else:
r.append(indent(f'echo Warning: {msg_body}\\n', indent_str))
# Close 'else' operators
for i in range(len(roms[target]), 0, -1):
r.append(indent('end', base_ident * i))
r.append('set confirm on')
r.append('end')
r.append('')
return '\n'.join(r)
raise FatalError(f'{ESP_ROM_INFO_FILE} file not found. Please check IDF integrity.')
def generate_gdbinit_files(gdb: str, gdbinit: Optional[str], project_desc: Dict[str, Any]) -> None:
app_elf = get_normalized_path(os.path.join(project_desc['build_dir'], project_desc['app_elf']))
if not os.path.exists(app_elf):
raise FatalError('ELF file not found. You need to build & flash the project before running debug targets')
# Recreate empty 'gdbinit' directory
gdbinit_dir = '/'.join([project_desc['build_dir'], 'gdbinit'])
if os.path.isfile(gdbinit_dir):
os.remove(gdbinit_dir)
elif os.path.isdir(gdbinit_dir):
shutil.rmtree(gdbinit_dir)
os.mkdir(gdbinit_dir)
# Prepare gdbinit for Python GDB extensions import
py_extensions = '/'.join([gdbinit_dir, 'py_extensions'])
with open(py_extensions, 'w') as f:
if is_gdb_with_python(gdb):
f.write(GDBINIT_PYTHON_TEMPLATE.format(sys_path=sys.path))
else:
f.write(GDBINIT_PYTHON_NOT_SUPPORTED)
# Prepare gdbinit for related ELFs symbols load
symbols = '/'.join([gdbinit_dir, 'symbols'])
with open(symbols, 'w') as f:
boot_elf = get_normalized_path(project_desc['bootloader_elf']) if 'bootloader_elf' in project_desc else None
if boot_elf and os.path.exists(boot_elf):
f.write(GDBINIT_BOOTLOADER_ADD_SYMBOLS.format(boot_elf=boot_elf))
else:
f.write(GDBINIT_BOOTLOADER_NOT_FOUND)
f.write(generate_gdbinit_rom_add_symbols(project_desc['target']))
f.write(GDBINIT_APP_ADD_SYMBOLS.format(app_elf=app_elf))
# Generate the gdbinit for target connect if no custom gdbinit is present
if not gdbinit:
gdbinit = '/'.join([gdbinit_dir, 'connect'])
with open(gdbinit, 'w') as f:
f.write(GDBINIT_CONNECT)
with open(os.path.join(gdbinit_dir, 'gdbinit'), 'w') as f:
f.write(GDBINIT_MAIN.format(py_extensions=py_extensions, symbols=symbols, connect=gdbinit))
def debug_cleanup() -> None:
print('cleaning up debug targets')
for t in processes['threads_to_join']:
@@ -375,8 +227,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
project_desc = get_project_desc(args, ctx)
if openocd_arguments is None:
# use default value if commands not defined in the environment nor command line
target = project_desc['target']
openocd_arguments = get_openocd_arguments(target)
openocd_arguments = project_desc.get('debug_arguments_openocd', '')
print(
'Note: OpenOCD cfg not found (via env variable OPENOCD_COMMANDS nor as a --openocd-commands argument)\n'
'OpenOCD arguments default to: "{}"'.format(openocd_arguments))
@@ -399,22 +250,60 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
processes['openocd_outfile_name'] = openocd_out_name
print('OpenOCD started as a background task {}'.format(process.pid))
def get_gdb_args(project_desc: Dict[str, Any]) -> List:
gdbinit = os.path.join(project_desc['build_dir'], 'gdbinit', 'gdbinit')
args = ['-x={}'.format(gdbinit)]
debug_prefix_gdbinit = project_desc.get('debug_prefix_map_gdbinit')
if debug_prefix_gdbinit:
args.append('-ix={}'.format(debug_prefix_gdbinit))
return args
def get_gdb_args(project_desc: Dict[str, Any], gdb_x: Tuple, gdb_ex: Tuple, gdb_commands: Optional[str]) -> List[str]:
# check if the application was built and ELF file is in place.
app_elf = os.path.join(project_desc.get('build_dir', ''), project_desc.get('app_elf', ''))
if not os.path.exists(app_elf):
raise FatalError('ELF file not found. You need to build & flash the project before running debug targets')
# debugger application name (xtensa-esp32-elf-gdb, riscv32-esp-elf-gdb, ...)
gdb_name = project_desc.get('monitor_toolprefix', '') + 'gdb'
gdb_args = [gdb_name]
gdbinit_files = project_desc.get('gdbinit_files')
if not gdbinit_files:
raise FatalError('Please check if the project was configured correctly ("gdbinit_files" not found in "project_description.json").')
gdbinit_files = sorted(gdbinit_files.items())
gdb_x_list = list(gdb_x)
gdb_x_names = [os.path.basename(x) for x in gdb_x_list]
# compile predefined gdbinit files options.
for name, path in gdbinit_files:
name = name[len('xx_'):]
if name == 'py_extensions':
if not is_gdb_with_python(gdb_name):
continue
if os.name == 'nt': # set PYTHONPATH for windows (already done inside bin-wrappers in linux/macos)
os.environ['PYTHONPATH'] = os.pathsep.join(sys.path[1:])
# Replace predefined gdbinit with user's if passed with the same name.
if name in gdb_x_names:
gdb_x_index = gdb_x_names.index(name)
gdb_args.append(f'-x={gdb_x_list[gdb_x_index]}')
gdb_x_list.pop(gdb_x_index)
continue
if name == 'connect' and gdb_x_list: # TODO IDF-11692
continue
gdb_args.append(f'-x={path}')
# append user-defined gdbinit files
for x in gdb_x_list:
gdb_args.append(f'-x={x}')
# add user-defined commands
if gdb_ex:
for ex in gdb_ex:
gdb_args.append('-ex')
gdb_args.append(ex)
# add user defined options
if gdb_commands:
gdb_args += shlex.split(gdb_commands)
return gdb_args
def _get_gdbgui_version(ctx: Context) -> Tuple[int, ...]:
subprocess_success = False
try:
completed_process = subprocess.run(['gdbgui', '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
subprocess_success = True
captured_output = completed_process.stdout.decode('utf-8', 'ignore')
subprocess_success = True
except FileNotFoundError:
# This is happening at least with Python 3.12 when gdbgui is not installed
subprocess_success = False
pass
if not subprocess_success or completed_process.returncode != 0:
if sys.version_info[:2] >= (3, 11) and sys.platform == 'win32':
@@ -427,41 +316,41 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'Please use "idf.py gdb" or debug in Eclipse/Vscode instead.')
raise FatalError('Error starting gdbgui. Please make sure gdbgui has been installed with '
'"install.{sh,bat,ps1,fish} --enable-gdbgui" and can be started. '
f'Error: {captured_output}', ctx)
f'Error: {captured_output if subprocess_success else "Unknown"}', ctx)
v = re.search(r'(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?', captured_output)
if not v:
raise SystemExit(f'Error: "gdbgui --version" returned "{captured_output}"')
return tuple(int(i) if i else 0 for i in (v[1], v[2], v[3], v[4]))
def gdbui(action: str, ctx: Context, args: PropertyDict, gdbgui_port: Optional[str], gdbinit: Optional[str],
require_openocd: bool) -> None:
def gdbui(action: str, ctx: Context, args: PropertyDict, gdbgui_port: Optional[str], gdbinit: Tuple,
ex: Tuple, gdb_commands: Optional[str], require_openocd: bool) -> None:
"""
Asynchronous GDB-UI target
"""
project_desc = get_project_desc(args, ctx)
local_dir = project_desc['build_dir']
gdb = project_desc['monitor_toolprefix'] + 'gdb'
generate_gdbinit_files(gdb, gdbinit, project_desc)
gdbgui_version = _get_gdbgui_version(ctx)
gdb_args_list = get_gdb_args(project_desc)
gdb_args = get_gdb_args(project_desc, gdbinit, ex, gdb_commands)
if gdbgui_version >= (0, 14, 0, 0):
# See breaking changes https://github.com/cs01/gdbgui/blob/master/CHANGELOG.md#01400, especially the
# replacement of command line arguments.
gdb_args = ' '.join(gdb_args_list)
args = ['gdbgui', '-g', ' '.join((gdb, gdb_args))]
gdbgui_args = ['gdbgui', '-g', ' '.join(gdb_args)]
else:
# this is a workaround for gdbgui
# gdbgui is using shlex.split for the --gdb-args option. When the input is:
# - '"-x=foo -x=bar"', would return ['foo bar']
# - '-x=foo', would return ['-x', 'foo'] and mess up the former option '--gdb-args'
# so for one item, use extra double quotes. for more items, use no extra double quotes.
gdb_args = '"{}"'.format(' '.join(gdb_args_list)) if len(gdb_args_list) == 1 else ' '.join(gdb_args_list)
args = ['gdbgui', '-g', gdb, '--gdb-args', gdb_args]
gdb = gdb_args[0]
gdb_args_list = gdb_args[1:]
gdb_args_str = '"{}"'.format(' '.join(gdb_args_list)) if len(gdb_args_list) == 1 else ' '.join(gdb_args_list)
gdbgui_args = ['gdbgui', '-g', gdb, '--gdb-args', gdb_args_str]
if gdbgui_port is not None:
args += ['--port', gdbgui_port]
gdbgui_args += ['--port', gdbgui_port]
gdbgui_out_name = os.path.join(local_dir, GDBGUI_OUT_FILE)
gdbgui_out = open(gdbgui_out_name, 'w')
env = os.environ.copy()
@@ -470,8 +359,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
# pygdbmi).
env['PURE_PYTHON'] = '1'
try:
print('Running: ', args)
process = subprocess.Popen(args, stdout=gdbgui_out, stderr=subprocess.STDOUT, bufsize=1, env=env)
print('Running: ', gdbgui_args)
process = subprocess.Popen(gdbgui_args, stdout=gdbgui_out, stderr=subprocess.STDOUT, bufsize=1, env=env)
except (OSError, subprocess.CalledProcessError) as e:
print(e)
raise FatalError('Error starting gdbgui', ctx)
@@ -509,14 +398,14 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
if task.name in ('gdb', 'gdbgui', 'gdbtui'):
task.action_args['require_openocd'] = True
def gdbtui(action: str, ctx: Context, args: PropertyDict, gdbinit: str, require_openocd: bool) -> None:
def gdbtui(action: str, ctx: Context, args: PropertyDict, gdbinit: Tuple, ex: Tuple, gdb_commands: str, require_openocd: bool) -> None:
"""
Synchronous GDB target with text ui mode
"""
gdb(action, ctx, args, False, 1, gdbinit, require_openocd)
gdb(action, ctx, args, False, 1, gdbinit, ex, gdb_commands, require_openocd)
def gdb(action: str, ctx: Context, args: PropertyDict, batch: bool, gdb_tui: Optional[int], gdbinit: Optional[str],
require_openocd: bool) -> None:
def gdb(action: str, ctx: Context, args: PropertyDict, batch: bool, gdb_tui: Optional[int], gdbinit: Tuple,
ex: Tuple, gdb_commands: Optional[str], require_openocd: bool) -> None:
"""
Synchronous GDB target
"""
@@ -524,14 +413,12 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
watch_openocd.start()
processes['threads_to_join'].append(watch_openocd)
project_desc = get_project_desc(args, ctx)
gdb = project_desc['monitor_toolprefix'] + 'gdb'
generate_gdbinit_files(gdb, gdbinit, project_desc)
args = [gdb, *get_gdb_args(project_desc)]
gdb_args = get_gdb_args(project_desc, gdbinit, ex, gdb_commands)
if gdb_tui is not None:
args += ['-tui']
gdb_args += ['-tui']
if batch:
args += ['--batch']
p = subprocess.Popen(args)
gdb_args += ['--batch']
p = subprocess.Popen(gdb_args)
processes['gdb'] = p
while True:
try:
@@ -600,9 +487,23 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'default': False,
}
gdbinit = {
'names': ['--gdbinit'],
'names': ['--gdbinit', '--x', '-x'],
'help': 'Specify the name of gdbinit file to use\n',
'default': None,
'multiple': True,
}
ex = {
'names': ['--ex', '-ex'],
'help':
('Execute given GDB command.\n'),
'default': None,
'multiple': True,
}
gdb_commands = {
'names': ['--gdb-commands', '--gdb_commands'],
'help':
('Command line arguments for gdb.\n'),
'default': None,
}
debug_actions = {
'global_action_callbacks': [global_callback],
@@ -642,7 +543,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'names': ['--gdb-tui', '--gdb_tui'],
'help': ('run gdb in TUI mode\n'),
'default': None,
}, gdbinit, fail_if_openocd_failed
}, gdbinit, ex, gdb_commands, fail_if_openocd_failed
],
'order_dependencies': ['all', 'flash'],
},
@@ -656,14 +557,14 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
('The port on which gdbgui will be hosted. Default: 5000\n'),
'default':
None,
}, gdbinit, fail_if_openocd_failed
}, gdbinit, ex, gdb_commands, fail_if_openocd_failed
],
'order_dependencies': ['all', 'flash'],
},
'gdbtui': {
'callback': gdbtui,
'help': 'GDB TUI mode.',
'options': [gdbinit, fail_if_openocd_failed],
'options': [gdbinit, ex, gdb_commands, fail_if_openocd_failed],
'order_dependencies': ['all', 'flash'],
},
'coredump-info': {