前言
我们在编程时会出现 父exe 调用 子exe 的情况,同时我们还需要调试 子exe 的代码。
因此,我尝试写一个示例,并尝试在 windows 和 linux(统信系统) 下调试子exe。
同时为了跨平台,我们使用 cmake 和 纯c++库 来编写代码。
调试时必须使用 DEBUG 来编译代码。
代码
- 代码结构
MultiProcDemo/
├── CMakeLists.txt # 主工程
├── parent/
│ ├── CMakeLists.txt # parent子工程
│ └── parent.cpp # parent源码
└── child/
├── CMakeLists.txt # child子工程
└── child.cpp # child源码
- 主 CMakeLists.txt
# CMakeList.txt: MultiProcDemo 的 CMake 项目,在此处包括源代码并定义
# 项目特定的逻辑。
#
cmake_minimum_required (VERSION 3.8)
project ("MultiProcDemo")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (MSVC)
# 禁用优化
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Od")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Od")
endif()
# 将所有可执行文件输出到统一目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_subdirectory(child) # 先编译 child
add_subdirectory(parent) # 后编译 parent
- parent CMakeLists.txt
add_executable(parent parent.cpp)
- parent.cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <cstdlib>
#if defined(_WIN32)
#include <windows.h>
#elif defined(__linux__)
#include <cstdlib>
#include <unistd.h>
#endif
void launchChildInNewTerminal() {
std::string command;
#if defined(_WIN32)
command = "start cmd /K .\\child.exe";
system(command.c_str());
#elif defined(__linux__)
command = "xterm -e ./child &";
system(command.c_str());
#endif
}
int main() {
launchChildInNewTerminal();
int count = 0;
for (int i = 0; i < 20; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
count++;
std::cout << "parent.exe: " << count << std::endl;
}
return 0;
}
- child CMakeLists.txt
add_executable(child child.cpp)
- child.cpp
#include <iostream>
#include <thread>
#include <chrono>
int main() {
int count = 0;
for (int i = 0; i < 20; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
count++;
std::cout << "child.exe: " << count << std::endl;
}
return 0;
}
windows 下调试代码三种方式
一、vs
我们使用下图的附加到进程来调试代码,但由于我的环境有问题,vs调试不了。
二、windbg.exe
这是 vs 安装开发组件时自动安装的调试软件,不需要其它 IDE ,我们就可以使用它来调试代码。
- 路径 C:\Program Files (x86)\Windows Kits\10\Debuggers\x64
- 打开 wingdb,将生成的 pdb 路径告诉 wingdb。“file”->“source file path…”
- 运行 parent.exe,通过 “任务管理器” 获取 child 的 PID,我的是 16264。
- wingdb 根据 PID 附加到进程 .“file”->“attach to a process…”
- 打断点
注意:
bu `child.cpp:8`
这里的 不是键盘右边的单引号,而是键盘左上角 esc 下的那个波浪键。
- 打完断点后,输入命令 ‘g’ 继续运行,左边会自动显示代码,我们可以用 F10 F11 来调试代码。
常用命令:
一、基本流程控制与运行
命令 说明 示例
g 继续运行程序 g
Ctrl+Break 暂停正在运行的程序(中断到调试器) -
q 退出调试器 q
二、断点管理
命令 说明 示例
bp <address/function> 在指定地址/函数设置普通断点(需模块已加载) bp MyApp!main
bu 设置延迟断点(模块未加载时自动延迟绑定) bu MyApp!CrashFunction
bl 列出所有断点(含 ID 和状态) bl
bc 清除指定断点 bc 1
bd 禁用断点 bd 1
be 启用断点 be 1
三、查看代码与内存
命令 说明 示例
u
.asm no_code_bytes 反汇编时不显示机器码(简化输出) .asm no_code_bytes
d* 查看内存数据(db字节/dw字/dd双字等) db 0012ff44 (查看字节)
!address 查看内存地址属性(可读/可写/所属模块等) !address 0012ff44
四、堆栈与线程管理
命令 说明 示例
k 显示当前调用栈(简略) k
kvn 显示详细调用栈(带参数和框架编号) kvn
~ 列出所有线程 ~
~s 切换到指定线程 ~2s (切换到线程 2)
dv 查看当前函数的局部变量 dv
.frame 切换调用栈帧 .frame 1
五、符号与模块管理
命令 说明 示例
.sympath+
.reload /f 强制重新加载所有符号 .reload /f
lm 列出所有已加载模块 lm
x ! 查找符号地址 x MyApp! Crash (搜索带 Crash 的函数/变量)
.load 加载调试器扩展(如 SOS) .load sos.dll (用于 .NET 调试)
六、变量与表达式
命令 说明 示例
? 计算表达式值 ? eax*4
r 查看/修改寄存器 r eax=5
dt 按类型解析内存(如结构体) dt MyStruct 0012ff44
dd L 连续查看内存块 dd 0012ff44 L4 (查看 4 个双字)
七、分析崩溃与异常
命令 说明 示例
!analyze -v 自动分析崩溃原因(包括错误码和堆栈) !analyze -v
.exr 查看异常记录 .exr 0012fe34
.dump /ma 生成完整内存转储文件 .dump /ma C:\crash.dmp
!peb 查看进程环境块(PEB)信息 !peb
八、扩展命令(SOS, etc.)
命令 说明 示例
!clrstack (SOS)查看托管代码堆栈 !clrstack
!dumpheap (SOS)查看托管堆对象统计 !dumpheap -stat
!threads (SOS)列出所有托管线程 !threads
!handle 查看进程句柄信息 !handle 4
九、典型调试流程示例
- 调试程序崩溃
# 启动 WinDbg 并附加到进程
windbg -pn MyApp.exe
# 自动分析崩溃原因
!analyze -v
# 查看崩溃时的调用栈
kvn
# 保存转储文件
.dump /ma C:\crash.dmp
- 调试死锁(多线程)
# 列出所有线程
~
# 切换到线程 3
~3s
# 查看该线程的调用栈
kvn
# 检查等待的锁
!locks
三、Qt
虽然是 Qt ,但是它在 windows 下使用的是 vs 的 msvc 编译器,调试也用的是 cdb,所以和上面的 vs 表现应当相同。
- 编译一次代码,选择 “调试”->“开始调试”->“attach to unstarted application”。
- 在下图蓝色框中将 child.exe 和具体路径填入,点击 “start watching”
- 我们在 child.cpp 里打上断点。
- 手动去文件夹下启动 parent.exe。
- 快速切换至 Qt,点击“确定”。
- 可开始用 F10 F11进行调试了
linux 下调试代码两种方式
一、Qt
步骤和上面一样,Qt不愧是跨平台,就是好用。
二、GDB
如果确实没有装 Qt,这时候就得用 gdb 来调试。
- 编译源码
主 cmakeLists.txt 同目录下
mkdir build
cd build/
cmake .. -DCMAKE_BUILD_TYPE=Debug
make -j4
- 进入编译出的程序目录下
- 打开终端
执行 ./parent
可看到 parent 在一个终端中运行,child 在另一个终端运行。 - 查找 child PID
- 根据 PID 启动 gdb
- 使用 gdb 命令进行调试
常用gdb命令如下:
(gdb) break child.cpp:8 # 在 child.cpp 第 8 行设置断点
(gdb) break main # 在 main 函数设置断点
(gdb) run # 启动程序
(gdb) next # 单步执行(不进入函数)
(gdb) step # 单步执行(进入函数)
(gdb) print x # 打印变量 x
(gdb) info breakpoints # 查看所有断点
(gdb) continue # 继续运行到下一个断点
(gdb) quit # 退出 GDB