mirror of
https://github.com/espressif/esp-idf.git
synced 2025-08-17 07:09:37 +00:00
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:
@@ -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': {
|
||||
|
Reference in New Issue
Block a user