正如终端调试哪家强?所言,Emacs gud mode应该是终端下目前最强的GDB前端。 随着Emacs版本更新,该文中的部分配置已失效,本文将探讨适用于Emacs 29.1的gud mode配置。 另外,gud mode不提供“开始或结束调试”和“toggle断点”功能的原生支持,本文将给出这两个功能的实现方法。
本文接下来将介绍Emacs GDB的配置方法,包括调试相关的各个buffer布局,以及调试状态下的快捷键配置。 在继续阅读本文之前,建议先阅读终端调试哪家强?以了解Emacs GDB的基础用法。
1. 窗口布局
使用M-x gdb进入调试状态后,默认情况下只有一个gdb cmdline窗口,而这通常不能满足调试需求。 此时可以通过gdb-many-windows命令来启用多窗口布局,或者也可以gdb-many-windows变量设置为t来默认启用之。
默认的多窗口布局通常也不能满足大家的个性化定制需求。 gdb-save-window-configuration命令可以将通过拖拽方式等配置好的布局保存到配置文件,而gdb-default-window-configuration-file变量则可以将某个配置文件设置为默认布局。 但是这个方法的缺点为通过配置文件加载的默认布局不能较好地适应终端窗口大小变化:改变终端窗口大小可能会导致各个调试buffer窗口相对比例失衡。
Emacs是通过gdb-setup-windows来设置窗口默认布局,因此可以给其添加advice来达到改变默认布局的目的。 这个方法可以较好地适应终端窗口大小变化,但advice的本质导致其可能无法兼容将来的Emacs版本。 下面的advice代码可以生成下图所示的多窗口布局。
(advice-add 'gdb-setup-windows :around (lambda (orig-fun &rest args) (my-gdb-setup-windows))) ;; https://github.com/emacs-mirror/emacs/blob/emacs-29.1/lisp/progmodes/gdb-mi.el#L4986 (defun my-gdb-setup-windows () (gdb-get-buffer-create 'gdb-locals-values-buffer) (gdb-get-buffer-create 'gdb-locals-buffer) (gdb-get-buffer-create 'gdb-stack-buffer) (gdb-get-buffer-create 'gdb-breakpoints-buffer) (set-window-dedicated-p (selected-window) nil) (switch-to-buffer gud-comint-buffer) (delete-other-windows) (let* ((win11 (selected-window)) (win31 (split-window nil (/ (* (window-height) 3) 4))) (win21 (split-window nil (/ (window-height) 3)))) ;; 1 (let* ((win13 (split-window-right nil win11)) (win12 (split-window-right (/ (* (window-width) 2) 3) win11))) (gdb-set-window-buffer (gdb-get-buffer-create 'gdb-inferior-io) nil win12) (gdb-set-window-buffer (gdb-locals-buffer-name) nil win13)) ;; 2 (select-window win21) (set-window-buffer win21 (or (gdb-get-source-buffer) (list-buffers-noselect))) (setq gdb-source-window-list (list (selected-window))) (let* ((win22 (split-window-right nil win21)) (win23 (split-window-right (/ (* (window-width) 3) 4) win22))) (gdb-set-window-buffer (gdb-get-buffer-create 'gdb-disassembly-buffer) nil win22) (gdb-set-window-buffer (gdb-get-buffer-create 'gdb-registers-buffer) nil win23)) ;; 3 (select-window win31) (gdb-set-window-buffer (gdb-stack-buffer-name)) (let ((win32 (split-window-right))) (gdb-set-window-buffer (gdb-get-buffer-create 'gdb-memory-buffer) nil win32)) ;; (select-window win21)))
2. 快捷键
Emacs通过gud mode来支持GDB, gud mode提供了很多调试相关命令,而这些命令可以通过定制gud-minor-mode-map以被绑定到快捷键上。
例如,(define-key gud-minor-mode-map (kbd "<f6>") #'gud-next)
即可将gud-next绑定到F6键上。
常见的gud mode调试命令如下:
gud-run | 开始调试 |
gud-kill | 结束调试 |
gud-break | 设置断点 |
gud-remove | 清除断点 |
gud-next | 单步执行,无视函数 |
gud-nexti | 单步执行,进入函数 |
gud-util | 执行到光标所在前行 |
gud-up | 前移stack frame |
gud-down | 后移stack frame |
GUD mode默认没有提供“开始或结束调试”命令和“toggle断点”命令,因此无法直接使用默认gud命令来实现这些IDE中常见的快捷键。 所幸这两个命令都可以被实现,下面将给出这两个命令的一种实现方法。
2.1. 开始或结束调试 (my-gud-run-cont)
gdb-inferior-status变量可以被用来判断当前是否处于调试状态,可以基于其实现本命令,代码如下:
;; https://emacs.stackexchange.com/questions/37987/bindings-for-source-code-when-debugging-in-gdb-gud (defun my-gud-run-cont (arg) (interactive "p") (if (assoc-string gdb-inferior-status '("breakpoint-hit" "end-stepping-range" "watchpoint-trigger" "read-watchpoint-trigger" "access-watchpoint-trigger" "function-finished" "location-reached" "watchpoint-scope" "signal-received")) (gud-cont arg) (gud-run arg)))
2.2. Toggle断点 (my-gud-toggle-breakpoint)
若当前行具有断点,则行首字符的overlay属性中将具有断点信息。 可以根据该原理来实现本命令,具体代码如下:
(defun my-gud-breakpoint-overlay-get () (let* ((pos (line-beginning-position))) (cl-loop for overlay in (overlays-in pos pos) when (or (overlay-get overlay 'put-break) (overlay-get overlay 'put-image)) return overlay))) (defun my-gud-set-clear-breakpoint (&optional tbreak) (interactive "P") (if (overlayp (my-gud-breakpoint-overlay-get)) (gud-remove nil) (if tbreak (gud-tbreak nil) (gud-break nil)))) (defun my-gud-toggle-breakpoint (arg) (interactive "p") (when-let* ((overlay (my-gud-breakpoint-overlay-get)) (obj (overlay-get overlay 'before-string))) (unless (get-text-property 0 'gdb-bptno obj) ;; terminal (when-let* ((display-prop (get-text-property 0 'display obj)) (display-obj (nth 1 display-prop))) (setq obj display-obj))) (let* ((enabled (get-text-property 0 'gdb-enabled obj)) (bptno (get-text-property 0 'gdb-bptno obj))) (gud-basic-call (concat (if enabled "-break-disable " "-break-enable ") bptno)))))