首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
10
57
[原创]ptrace学习: 阅读 lldb 相关源码
发表于: 2025-10-12 09:22
3710
[原创]ptrace学习: 阅读 lldb 相关源码
nothing233
1
2025-10-12 09:22
3710
## 简介 阅读 lldb 源码学习 ptrace 的使用. ## 前置阅读(建议读一下) - <a href="elink@5ecK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6*7K9s2g2S2L8X3I4S2L8W2)9J5k6i4A6Z5K9h3S2#2i4K6u0W2j5$3!0E0i4K6u0r3M7q4)9J5c8U0j5#2x3K6x3^5y4e0t1$3y4l9`.`.">必读榜 1</a> - <a href="elink@fe4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6*7K9s2g2S2L8X3I4S2L8W2)9J5k6i4A6Z5K9h3S2#2i4K6u0W2j5$3!0E0i4K6u0r3M7q4)9J5c8U0t1^5z5o6M7^5x3e0M7$3x3K6M7@1">必读榜 2</a> - <a href="elink@ca9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2S2L8Y4q4#2j5h3&6C8k6g2)9J5k6h3y4G2L8g2)9J5c8Y4m8G2M7%4c8Q4x3V1k6A6k6q4)9J5c8U0t1K6x3e0l9%4z5q4)9J5x3$3R3K6i4K6u0V1x3H3`.`.">必读榜 3</a> ## 在 lldb 中的 ptrace 来到 `lldb/source/Plugins/Process/Linux` 目录中. ### `NativeProcessLinux.h` `NativeProcessLinux.cpp` 先看头文件, 了解下实现了那些功能: ```cpp namespace lldb_private { class Status; class Scalar; namespace process_linux { /// \class NativeProcessLinux /// 管理与被调试进程(inferior)的通信 /// /// 构造时, 该类准备并启动一个用于调试的被调试进程 /// /// 被调试进程状态的变化会被广播出去 class NativeProcessLinux : public NativeProcessELF, private NativeProcessSoftwareSingleStep { public: class Manager : public NativeProcessProtocol::Manager { public: Manager(MainLoop &mainloop); /// 启动一个新进程进行调试 /// \param[in] launch_info 进程启动信息 /// \param[in] native_delegate 调试回调接口 /// \return 成功则返回进程实例, 失败则返回错误信息 llvm::Expected<std::unique_ptr<NativeProcessProtocol>> Launch(ProcessLaunchInfo &launch_info, NativeDelegate &native_delegate) override; /// 附加到一个已运行的进程 /// \param[in] pid 目标进程ID /// \param[in] native_delegate 调试回调接口 /// \return 成功则返回进程实例, 失败则返回错误信息 llvm::Expected<std::unique_ptr<NativeProcessProtocol>> Attach(lldb::pid_t pid, NativeDelegate &native_delegate) override; /// 获取支持的扩展功能 Extension GetSupportedExtensions() const override; /// 添加进程到管理列表 void AddProcess(NativeProcessLinux &process) { m_processes.insert(&process); } /// 从管理列表移除进程 void RemoveProcess(NativeProcessLinux &process) { m_processes.erase(&process); } /// 收集指定线程的事件, 必要时等待事件发生 void CollectThread(::pid_t tid); private: /// 处理SIGCHLD信号的句柄 MainLoop::SignalHandleUP m_sigchld_handle; /// 正在管理的进程集合 llvm::SmallPtrSet<NativeProcessLinux *, 2> m_processes; /// 尚未被任何进程认领的线程(事件) llvm::DenseSet<::pid_t> m_unowned_threads; /// SIGCHLD信号处理函数 void SigchldHandler(); }; // NativeProcessProtocol接口实现 ~NativeProcessLinux() override { m_manager.RemoveProcess(*this); } /// 恢复进程执行, 根据resume_actions指定每个线程的恢复方式 Status Resume(const ResumeActionList &resume_actions) override; /// 暂停进程执行 Status Halt() override; /// 与被调试进程分离(停止调试但不终止进程) Status Detach() override; /// 向进程发送信号 Status Signal(int signo) override; /// 中断进程执行 Status Interrupt() override; /// 终止被调试进程 Status Kill() override; /// 获取指定内存地址的内存区域信息 Status GetMemoryRegionInfo(lldb::addr_t load_addr, MemoryRegionInfo &range_info) override; /// 从进程内存中读取数据 Status ReadMemory(lldb::addr_t addr, void *buf, size_t size, size_t &bytes_read) override; /// 向进程内存中写入数据 Status WriteMemory(lldb::addr_t addr, const void *buf, size_t size, size_t &bytes_written) override; /// 在被调试进程中分配内存 llvm::Expected<lldb::addr_t> AllocateMemory(size_t size, uint32_t permissions) override; /// 释放被调试进程中已分配的内存 llvm::Error DeallocateMemory(lldb::addr_t addr) override; /// 读取内存标签 Status ReadMemoryTags(int32_t type, lldb::addr_t addr, size_t len, std::vector<uint8_t> &tags) override; /// 写入内存标签 Status WriteMemoryTags(int32_t type, lldb::addr_t addr, size_t len, const std::vector<uint8_t> &tags) override; /// 更新线程列表, 返回更新后的线程数量 size_t UpdateThreads() override; /// 获取进程的架构信息 const ArchSpec &GetArchitecture() const override { return m_arch; } /// 设置断点 /// \param[in] addr 断点地址 /// \param[in] size 断点大小 /// \param[in] hardware 是否使用硬件断点 Status SetBreakpoint(lldb::addr_t addr, uint32_t size, bool hardware) override; /// 移除断点 Status RemoveBreakpoint(lldb::addr_t addr, bool hardware = false) override; /// 处理停止ID更新 void DoStopIDBumped(uint32_t newBumpId) override; /// 获取已加载模块的文件路径 Status GetLoadedModuleFileSpec(const char *module_path, FileSpec &file_spec) override; /// 获取文件的加载地址 Status GetFileLoadAddress(const llvm::StringRef &file_name, lldb::addr_t &load_addr) override; /// 通过线程ID获取线程对象 NativeThreadLinux *GetThreadByID(lldb::tid_t id); /// 获取当前线程 NativeThreadLinux *GetCurrentThread(); /// 获取auxv数据(辅助向量, 包含进程启动信息) llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> GetAuxvData() const override { return getProcFile(GetID(), "auxv"); } /// 跟踪相关方法 /// 这些方法实现jLLDBTrace协议包 /// \{ /// 开始跟踪 llvm::Error TraceStart(llvm::StringRef json_request, llvm::StringRef type) override; /// 停止跟踪 llvm::Error TraceStop(const TraceStopRequest &request) override; /// 获取跟踪状态 llvm::Expected<llvm::json::Value> TraceGetState(llvm::StringRef type) override; /// 获取跟踪的二进制数据 llvm::Expected<std::vector<uint8_t>> TraceGetBinaryData(const TraceGetBinaryDataRequest &request) override; /// 获取支持的跟踪功能 llvm::Expected<TraceSupportedResponse> TraceSupported() override; /// \} // 供NativeRegisterContext派生类使用的接口 /// ptrace系统调用的封装函数 static Status PtraceWrapper(int req, lldb::pid_t pid, void *addr = nullptr, void *data = nullptr, size_t data_size = 0, long *result = nullptr); /// 检查是否支持硬件单步执行 bool SupportHardwareSingleStepping() const; /// 将指定线程的siginfo_t结构写入到siginfo指向的内存区域 Status GetSignalInfo(lldb::tid_t tid, void *siginfo) const; protected: /// 获取软件断点的陷阱指令 llvm::Expected<llvm::ArrayRef<uint8_t>> GetSoftwareBreakpointTrapOpcode(size_t size_hint) override; /// 调用系统调用 llvm::Expected<uint64_t> Syscall(llvm::ArrayRef<uint64_t> args); private: /// 进程管理器引用 Manager &m_manager; /// 架构信息 ArchSpec m_arch; /// 内存区域支持状态(延迟计算) LazyBool m_supports_mem_region = eLazyBoolCalculate; /// 内存区域缓存 std::vector<std::pair<MemoryRegionInfo, FileSpec>> m_mem_region_cache; /// 等待通知的线程ID lldb::tid_t m_pending_notification_tid = LLDB_INVALID_THREAD_ID; /// 被调试进程中分配的内存(地址到大小的映射) llvm::DenseMap<lldb::addr_t, lldb::addr_t> m_allocated_memory; // 私有实例方法 /// 构造函数 NativeProcessLinux(::pid_t pid, int terminal_fd, NativeDelegate &delegate, const ArchSpec &arch, Manager &manager, llvm::ArrayRef<::pid_t> tids); /// 附加到进程, 返回已附加的线程列表 static llvm::Expected<std::vector<::pid_t>> Attach(::pid_t pid); /// 设置默认的ptrace选项 static Status SetDefaultPtraceOpts(const lldb::pid_t); /// 处理等待状态 bool TryHandleWaitStatus(lldb::pid_t pid, WaitStatus status); /// 监视回调函数 void MonitorCallback(NativeThreadLinux &thread, WaitStatus status); /// 处理SIGTRAP信号 void MonitorSIGTRAP(const siginfo_t &info, NativeThreadLinux &thread); /// 处理跟踪事件 void MonitorTrace(NativeThreadLinux &thread); /// 处理断点事件 void MonitorBreakpoint(NativeThreadLinux &thread); /// 处理监视点事件 void MonitorWatchpoint(NativeThreadLinux &thread, uint32_t wp_index); /// 处理信号事件 void MonitorSignal(const siginfo_t &info, NativeThreadLinux &thread); /// 检查是否存在指定线程(无锁版本) bool HasThreadNoLock(lldb::tid_t thread_id); /// 停止跟踪指定线程 void StopTrackingThread(NativeThreadLinux &thread); /// 创建新线程 /// /// 如果进程跟踪已启用且线程无法被跟踪, /// 则线程会以eStopReasonProcessorTrace状态停止, /// 并使整个进程停止 /// /// \param[in] resume /// 如果没有跟踪错误, 若为true则创建后恢复线程执行, /// 若为false则使线程以SIGSTOP状态停止 NativeThreadLinux &AddThread(lldb::tid_t thread_id, bool resume); /// 如果进程跟踪已启用, 开始跟踪新线程 /// /// 跟踪机制应修改此方法以提供新线程的自动跟踪 Status NotifyTracersOfNewThread(lldb::tid_t tid); /// 在线程销毁事件中停止跟踪 /// /// 跟踪机制应修改此方法以提供销毁线程的自动跟踪停止 Status NotifyTracersOfThreadDestroyed(lldb::tid_t tid); /// 通知跟踪器进程即将恢复执行 void NotifyTracersProcessWillResume() override; /// 通知跟踪器进程已停止 void NotifyTracersProcessDidStop() override; /// 将与指定线程ID对应的原始事件消息代码(对应PTRACE_GETEVENTMSG) /// 写入到message指向的内存 Status GetEventMessage(lldb::tid_t tid, unsigned long *message); /// 通知线程死亡 void NotifyThreadDeath(lldb::tid_t tid); /// 与指定线程分离 Status Detach(lldb::tid_t tid); /// 请求所有仍在运行的线程停止.设置延迟的代理通知, /// 当所有线程报告停止后触发.triggering_tid将被设为当前线程(主要停止原因) void StopRunningThreads(lldb::tid_t triggering_tid); /// 如果所有线程都已停止, 通知代理 void SignalIfAllThreadsStopped(); /// 恢复指定线程, 可选择传递信号.恢复操作类型(继续、单步)取决于state参数 Status ResumeThread(NativeThreadLinux &thread, lldb::StateType state, int signo); /// 线程已创建的处理 void ThreadWasCreated(NativeThreadLinux &thread); /// SIGCHLD信号处理 void SigchldHandler(); /// 填充内存区域缓存 Status PopulateMemoryRegionCache(); /// 管理Intel PT进程和线程跟踪 IntelPTCollector m_intel_pt_collector; /// 处理类似clone()的事件 bool MonitorClone(NativeThreadLinux &parent, lldb::pid_t child_pid, int event); }; } // namespace process_linux } // namespace lldb_private ``` 发现 `NativeProcessLinux` 类的构造方法是一个私有方法, 找了一下发现 `NativeProcessLinux::Manager` 中的 `Launch` `Attach` 才是入口, 先看一下 `Launch`: #### Launch ```cpp /// 启动新的被调试进程(inferior)并初始化调试环境 /// \param[in] launch_info 进程启动参数(包含可执行路径、命令行参数、环境变量等) /// \param[in] native_delegate 调试事件回调接口(用于通知进程状态变化、线程创建等) /// \return 成功则返回封装了 NativeProcessLinux 实例的智能指针, 失败则返回错误信息 llvm::Expected<std::unique_ptr<NativeProcessProtocol>> NativeProcessLinux::Manager::Launch(ProcessLaunchInfo &launch_info, NativeDelegate &native_delegate) { // 获取 POSIX 进程相关的日志对象(用于调试日志输出) Log *log = GetLog(POSIXLog::Process); // 记录进程启动参数到日志(如可执行路径、命令行参数等, 便于调试排查) MaybeLogLaunchInfo(launch_info); Status status; // 用于存储操作状态(成功/失败及错误信息) // 1. 通过 POSIX fork 机制启动新进程 // ProcessLauncherPosixFork 是 POSIX 平台的进程启动器, 内部通过 fork/exec 流程创建新进程 // LaunchProcess 会根据 launch_info 配置新进程, 并返回进程 ID(pid) ::pid_t pid = ProcessLauncherPosixFork() .LaunchProcess(launch_info, status) .GetProcessId(); // 记录新进程的 PID 到日志 LLDB_LOG(log, "pid = {0:x}", pid); // 检查进程启动是否失败(如可执行文件不存在、权限不足等) if (status.Fail()) { LLDB_LOG(log, "failed to launch process: {0}", status); // 将 Status 错误转换为 llvm::Error 返回(符合方法返回类型要求) return status.ToError(); } // 2. 等待子进程(被调试进程)在 execve 调用时触发停止 // 新进程启动后会执行 execve 加载目标程序, 而由于 ptrace(PTRACE_TRACEME) 配置, // execve 会触发 SIGTRAP 使进程停止, 此时父进程(LLDB)需要等待该停止事件 int wstatus = 0; // 存储 waitpid 的返回状态(用于判断进程状态) // 调用 waitpid 等待指定 PID 的进程状态变化, RetryAfterSignal 处理被信号中断的情况 ::pid_t wpid = llvm::sys::RetryAfterSignal(-1, ::waitpid, pid, &wstatus, 0); // 断言:确保等待到的是目标进程(防止出现逻辑错误导致等待到其他进程) assert(wpid == pid); (void)wpid; // 消除未使用变量的编译警告(release 模式下 assert 可能被禁用) // 检查进程是否因停止事件(如 SIGTRAP)而暂停, 若不是则调试同步失败 if (!WIFSTOPPED(wstatus)) { // 解码 wstatus 得到具体状态(如退出、信号终止等)并记录日志 LLDB_LOG(log, "Could not sync with inferior process: wstatus={1}", WaitStatus::Decode(wstatus)); // 返回同步失败的错误信息 return llvm::make_error<StringError>("Could not sync with inferior process", llvm::inconvertibleErrorCode()); } LLDB_LOG(log, "inferior started, now in stopped state"); // 日志记录:被调试进程已启动并停止 // 3. 为被调试进程设置默认的 ptrace 选项 // 例如设置 PTRACE_O_TRACECLONE、PTRACE_O_TRACEEXEC 等, 确保能跟踪线程创建、exec 等事件 status = SetDefaultPtraceOpts(pid); if (status.Fail()) { LLDB_LOG(log, "failed to set default ptrace options: {0}", status); return status.ToError(); } // 4. 确定被调试进程的架构信息(如 x86_64、ARM64、RISCV64 等) llvm::Expected<ArchSpec> arch_or = NativeRegisterContextLinux::DetermineArchitecture(pid); // 若架构判断失败返回错误 if (!arch_or) return arch_or.takeError(); // 5. 创建并返回 NativeProcessLinux 实例 // 释放 PTY 主文件描述符(用于被调试进程的标准输入输出交互), 传递给构造函数 // 初始线程列表仅包含主线程(PID 与 TID 相同, Linux 中主线程 TID 等于进程 PID) return std::unique_ptr<NativeProcessLinux>(new NativeProcessLinux( pid, // 被调试进程 PID launch_info.GetPTY().ReleasePrimaryFileDescriptor(), // PTY 主文件描述符 native_delegate, // 调试事件回调接口 *arch_or, // 进程架构信息 *this, // 当前 Manager 实例引用(用于进程管理) {pid} // 初始线程列表(主线程 TID) )); } ``` 整体浏览下来逻辑如下: - 根据 `launch_info` 创建新进程, 也就是子进程, 然后原进程, 也就是父进程等待子进程的信号(这个信号通过内核传递, PCB 中有信号队列用于暂存接受的信号, 在一些特定时机处理这些信号), 即 `子进程触发信号→内核记录→内核通知父进程` - 设置 ptrace 功能选项, 获取机构信息 - 返回 `NativeProcessLinux` 实例 然我们仔细看一下实现, 首先是 `LaunchProcess`, 看看如何创建子进程, 创建后都做了什么: ```cpp /// 使用 POSIX 的 fork 机制启动新进程, 并返回进程句柄 /// \param[in] launch_info 进程启动参数(包含可执行路径、命令行参数、环境变量等) /// \param[out] error 输出参数, 用于返回启动过程中的错误信息 /// \return 成功则返回新进程的 HostProcess 对象, 失败则返回空对象 HostProcess ProcessLauncherPosixFork::LaunchProcess(const ProcessLaunchInfo &launch_info, Status &error) { // 创建一个管道(pipe), 用于子进程向父进程报告启动过程中的错误 PipePosix pipe; // 子进程是否继承管道文件描述符:这里设为 false, 避免子进程意外操作管道 const bool child_processes_inherit = false; // 初始化管道, 若失败则返回空进程 error = pipe.CreateNew(child_processes_inherit); if (error.Fail()) return HostProcess(); // 将通用启动参数转换为 fork 专用的启动信息(包含环境变量、工作目录等细节) const ForkLaunchInfo fork_launch_info(launch_info); // 调用 POSIX 的 fork() 创建子进程 ::pid_t pid = ::fork(); if (pid == -1) { // fork 失败:记录错误信息(如权限不足、系统资源耗尽等) error.SetErrorStringWithFormatv("Fork failed with error message: {0}", llvm::sys::StrError()); return HostProcess(LLDB_INVALID_PROCESS_ID); } if (pid == 0) { // 子进程执行分支 // 关闭子进程中管道的读端(子进程只需要写端来报告错误) pipe.CloseReadFileDescriptor(); // 调用子进程处理函数:执行程序加载(execve)并处理可能的错误 // 传入管道的写端文件描述符, 用于出错时向父进程写错误信息 ChildFunc(pipe.ReleaseWriteFileDescriptor(), fork_launch_info); } // 父进程执行分支 // 关闭父进程中管道的写端(父进程只需要读端来接收子进程的错误信息) pipe.CloseWriteFileDescriptor(); // 读取子进程通过管道发送的错误信息(如果有的话) llvm::SmallString<0> buf; // 动态缓冲区, 用于存储错误信息 size_t pos = 0; // 当前已读取的字节数 ssize_t r = 0; // 单次 read 调用的返回值 do { pos += r; // 扩展缓冲区大小(每次增加 100 字节, 避免频繁扩容) buf.resize_for_overwrite(pos + 100); // 读取管道内容:RetryAfterSignal 处理被信号中断的情况 r = llvm::sys::RetryAfterSignal(-1, read, pipe.GetReadFileDescriptor(), buf.begin() + pos, buf.size() - pos); } while (r > 0); // 持续读取直到子进程关闭写端(r=0)或出错(r=-1) assert(r != -1); // 断言读取过程无错误(若失败则触发调试断点) // 调整缓冲区大小为实际读取的字节数 buf.resize(pos); // 如果缓冲区为空, 说明子进程启动成功(未发送错误信息) if (buf.empty()) return HostProcess(pid); // 返回包含子进程 PID 的 HostProcess 对象 // 若缓冲区非空, 说明子进程启动失败:将错误信息存入 error error.SetErrorString(buf); // 等待子进程退出(避免僵尸进程):忽略退出状态(已通过管道获取错误信息) llvm::sys::RetryAfterSignal(-1, waitpid, pid, nullptr, 0); // 返回空进程对象, 表示启动失败 return HostProcess(); } ``` 通过 fork 创建子进程, 并将子进程的启动参数 `launch_info` 包装为 `ForkLaunchInfo` 类的实例 `fork_launch_info`, 再跟入 `ChildFunc` 函数: ```cpp /// 子进程专属执行函数, 完成环境配置后加载目标程序, 执行失败则通过管道返回错误 /// [[noreturn]] 标记:函数不会返回(要么成功执行 execve 替换进程, 要么调用 ExitWithError 退出) /// \param[in] error_fd 用于向父进程传递错误信息的管道写端文件描述符 /// \param[in] info 子进程启动配置信息(包含文件操作、工作目录、调试开关等) [[noreturn]] static void ChildFunc(int error_fd, const ForkLaunchInfo &info) { // 1. 配置进程组(若需要独立进程组) // 独立进程组用于避免子进程受父进程终端信号(如 Ctrl+C)影响, 常见于调试场景 if (info.separate_process_group) { // setpgid(0, 0):创建新进程组, 子进程成为组长(参数 0 表示当前进程 PID) if (setpgid(0, 0) != 0) ExitWithError(error_fd, "setpgid"); // 执行失败, 向父进程报告错误后退出 } // 2. 执行预定义的文件操作(如关闭、复制、打开文件描述符) // 这些操作来自 ProcessLaunchInfo, 用于配置子进程的标准输入/输出、日志文件等 for (const ForkFileAction &action : info.actions) { switch (action.action) { case FileAction::eFileActionClose: // 关闭指定文件描述符(如父进程继承的无关文件句柄) if (close(action.fd) != 0) ExitWithError(error_fd, "close"); break; case FileAction::eFileActionDuplicate: // 复制文件描述符(如将标准输出重定向到日志文件:dup2(log_fd, STDOUT_FILENO)) if (dup2(action.fd, action.arg) == -1) ExitWithError(error_fd, "dup2"); break; case FileAction::eFileActionOpen: // 打开指定路径的文件, 并将文件描述符设置为 action.fd(如打开配置文件) DupDescriptor(error_fd, action.path.c_str(), action.fd, action.arg); break; case FileAction::eFileActionNone: // 无操作, 跳过 break; } } // 3. 切换子进程的工作目录(若配置了工作目录) if (!info.wd.empty() && 0 != ::chdir(info.wd.c_str())) ExitWithError(error_fd, "chdir"); // 切换失败(如目录不存在), 报告错误后退出 // 4. 禁用地址空间随机化(ASLR)(若配置了禁用) // 禁用 ASLR 可让程序每次启动时内存地址固定, 便于调试(如断点地址不变) if (info.disable_aslr) DisableASLR(error_fd); // 执行禁用逻辑, 失败则报告错误 // 5. 清空信号掩码, 避免父进程的信号屏蔽影响子进程 // 父进程可能设置了信号屏蔽(如忽略某些信号), 子进程需重置为默认状态 sigset_t set; if (sigemptyset(&set) != 0 || // 初始化空信号集(不屏蔽任何信号) pthread_sigmask(SIG_SETMASK, &set, nullptr) != 0) // 应用信号集 ExitWithError(error_fd, "pthread_sigmask"); // 6. 调试模式专属配置(若启用调试) if (info.debug) { // 6.1 放弃 setgid 权限(安全措施) // 若父进程有 setgid 权限(允许切换组ID), 子进程调试时主动放弃, 避免权限滥用 if (setgid(getgid()) != 0) ExitWithError(error_fd, "setgid"); // 6.2 关闭无关文件描述符(避免句柄泄漏) // 仅调试场景执行:因为调试时子进程无需继承父进程的非标准文件句柄(如日志、网络连接) // 注:该逻辑非信号安全, 但调试场景下子进程启动时无多线程, 可安全执行 const llvm::StringRef proc_fd_path = "/proc/self/fd"; // 进程自身打开的文件描述符目录 std::error_code ec; bool result; // 检查 /proc/self/fd 是否存在(Linux 系统支持, 用于遍历所有打开的 FD) ec = llvm::sys::fs::is_directory(proc_fd_path, result); if (result) { std::vector<int> files_to_close; // 存储需要关闭的 FD 列表 // 遍历 /proc/self/fd 目录下的所有 FD(每个目录项对应一个打开的文件描述符) for (llvm::sys::fs::directory_iterator iter(proc_fd_path, ec), file_end; iter != file_end && !ec; iter.increment(ec)) { // 从路径中提取 FD 数值(如 "/proc/self/fd/5" 提取为 5) int fd = std::stoi(iter->path().substr(proc_fd_path.size() + 1)); // 不关闭的 FD 规则: // 1. 前 3 个 FD(0=stdin、1=stdout、2=stderr); // 2. 有预定义文件操作的 FD(如调试用的 PTY 句柄); // 3. 错误传递管道的写端(error_fd, 用于报告错误) if (fd > 2 && !info.has_action(fd) && fd != error_fd) files_to_close.push_back(fd); } // 关闭所有标记的无关 FD for (int file_to_close : files_to_close) close(file_to_close); } else { // 若 /proc/self/fd 不可用(如非 Linux 系统), 采用备选方案:遍历可能的 FD 范围 int max_fd = sysconf(_SC_OPEN_MAX); // 获取系统支持的最大 FD 数量 for (int fd = 3; fd < max_fd; ++fd) // 从 FD=3 开始检查(跳过 stdin/stdout/stderr) if (!info.has_action(fd) && fd != error_fd) close(fd); } // 6.3 启用调试跟踪:让父进程(LLDB)成为当前子进程的调试器 // PT_TRACE_ME:子进程声明“愿意被父进程调试”, 后续执行 execve 时会触发 SIGTRAP 信号 if (ptrace(PT_TRACE_ME, 0, nullptr, 0) == -1) ExitWithError(error_fd, "ptrace"); } // 7. 执行目标程序:替换当前子进程的代码段、数据段(核心步骤) // execve(path, argv, envp):加载 path 指向的可执行文件, 使用 argv 命令行参数和 envp 环境变量 // 若执行成功, 当前进程会被完全替换, 后续代码不会执行;若失败则进入下方错误处理 execve(info.argv[0], const_cast<char *const *>(info.argv), info.envp); // 8. Linux 平台专属:处理 ETXTBSY 错误(可执行文件被占用) #if defined(__linux__) if (errno == ETXTBSY) { // 错误原因:可执行文件被其他进程占用(如 Android 中 adb 守护进程未释放文件句柄) // 解决方案:等待 50ms(0.05 秒)后重试一次, 避免短暂占用导致的启动失败 usleep(50000); // 微秒级睡眠(50000us = 50ms) execve(info.argv[0], const_cast<char *const *>(info.argv), info.envp); // 再次尝试执行 } #endif // 9. execve 执行失败(到这里说明两次尝试都失败):报告错误后退出子进程 // 常见失败原因:可执行文件不存在、权限不足、文件格式错误等 ExitWithError(error_fd, "execve"); } ``` 针对启动参数做了很多处理, 其实关键的只有两步: - `ptrace(PT_TRACE_ME, 0, nullptr, 0)`: 让父进程成为当前子进程的调试器, PT_TRACE_ME 表明子进程愿意被父进程调试, 后续执行 execve 时会触发 SIGTRAP 信号, 遇到断点指令(如 int 3)会触发 SIGTRAP, 暂停执行并等待父进程处理 - `execve(info.argv[0], const_cast<char *const *>(info.argv), info.envp);`: 执行 `execve` 成功加载一个新的程序映像时, 内核会在新程序开始执行之前, 自动在该进程中插入一个陷阱(类似于执行了一个 int 3)触发 SIGTRAP 信号, 将 SIGTRAP 信号添加到子进程的 PCB 待处理信号队列, 立即暂停子进程(将子进程状态设为 TASK_STOPPED), 阻止子进程自己处理这个信号, 此时内核会把 SIGCHLD 信号存到父进程的 PCB 队列, 这个信号的作用只是 “提醒父进程:快去看看子进程怎么了” 让我们回来再看接着看 `NativeProcessLinux::Manager::Launch` 方法, 跟入 `SetDefaultPtraceOpts`: ```cpp Status NativeProcessLinux::SetDefaultPtraceOpts(lldb::pid_t pid) { long ptrace_opts = 0; // 用于存储ptrace选项的变量, 初始化为0 // 让子进程在退出时触发一个事件.这用于将子进程保持在未终结状态, 直到它被销毁 ptrace_opts |= PTRACE_O_TRACEEXIT; // 让跟踪器跟踪在目标进程中生成的线程 ptrace_opts |= PTRACE_O_TRACECLONE; // 让跟踪器在execve系统调用返回前通知我们(需要用于禁用传统的SIGTRAP信号生成) ptrace_opts |= PTRACE_O_TRACEEXEC; // 让跟踪器跟踪fork创建的子进程 ptrace_opts |= PTRACE_O_TRACEFORK; // 让跟踪器跟踪vfork创建的子进程 ptrace_opts |= PTRACE_O_TRACEVFORK; // 让跟踪器跟踪vfork完成事件, 以便在子进程完成内存共享后恢复断点 ptrace_opts |= PTRACE_O_TRACEVFORKDONE; // 调用ptrace包装函数, 应用上面设置的所有选项 return PtraceWrapper(PTRACE_SETOPTIONS, pid, nullptr, (void *)ptrace_opts); } // ptrace的包装函数, 用于捕获错误并记录调用.注意ptrace在出错时会设置errno, // 因为-1可能是一个有效的结果(例如对于PTRACE_PEEK*操作) Status NativeProcessLinux::PtraceWrapper(int req, lldb::pid_t pid, void *addr, void *data, size_t data_size, long *result) { Status error; // 用于存储操作状态的对象 long int ret; // 用于存储ptrace调用的返回值 // 获取日志对象, 用于记录ptrace相关操作 Log *log = GetLog(POSIXLog::Ptrace); // 显示ptrace操作相关的字节数据(用于调试日志) PtraceDisplayBytes(req, data, data_size); errno = 0; // 重置errno, 以便正确捕获ptrace的错误状态 // 根据不同的请求类型调用ptrace if (req == PTRACE_GETREGSET || req == PTRACE_SETREGSET) // 对于寄存器集操作, 需要特殊处理addr参数 ret = ptrace(static_cast<__ptrace_request>(req), static_cast<::pid_t>(pid), *(unsigned int *)addr, data); else // 普通ptrace调用 ret = ptrace(static_cast<__ptrace_request>(req), static_cast<::pid_t>(pid), addr, data); // 如果返回值为-1, 表示发生错误, 设置错误信息 if (ret == -1) error.SetErrorToErrno(); // 如果提供了结果指针, 将ptrace的返回值存入 if (result) *result = ret; // 记录ptrace调用的详细信息到日志 LLDB_LOG(log, "ptrace({0}, {1}, {2}, {3}, {4})={5:x}", req, pid, addr, data, data_size, ret); // 再次显示操作后的字节数据(用于调试日志) PtraceDisplayBytes(req, data, data_size); // 如果操作失败, 记录错误信息到日志 if (error.Fail()) LLDB_LOG(log, "ptrace() failed: {0}", error); return error; // 返回操作状态 } ``` `SetDefaultPtraceOpts` 方法用于设置 ptrace 的选项, 再由 `PtraceWrapper` 方法调用 ptrace, 并且 `PtraceWrapper` 方法还封装了一些日志, 错误处理的逻辑. 在回到 `NativeProcessLinux::Manager::Launch` 跟入 `NativeRegisterContextLinux::DetermineArchitecture`, 查看获取错误信息的实现, 这里进入了 `NativeRegisterContextLinux*.cpp` 系列文件, `NativeRegisterContextLinux.cpp` 与 `NativeRegisterContextLinux.h` 提供统一接口, 其他文件对应不同架构的实例化.  这个跟了一下流程比较长, 并且与咱们的要探究的 ptrace 使用没什么太大的关系, 不再赘述, 感兴趣可以自己看一下. 再来看一下 `NativeProcessLinux::Manager::Attach`: #### Attach ```cpp // 附加到一个已运行的进程, 创建并返回NativeProcessLinux实例 // 参数: // pid - 要附加的进程ID // native_delegate - 用于接收进程事件通知的代理对象 // 返回值: 包含NativeProcessProtocol唯一指针的Expected对象, 成功则为进程实例, 失败则为错误信息 llvm::Expected<std::unique_ptr<NativeProcessProtocol>> NativeProcessLinux::Manager::Attach( lldb::pid_t pid, NativeProcessProtocol::NativeDelegate &native_delegate) { Log *log = GetLog(POSIXLog::Process); // 获取进程相关的日志对象 LLDB_LOG(log, "pid = {0:x}", pid); // 记录要附加的进程ID // 尝试附加到指定PID的进程 auto tids_or = NativeProcessLinux::Attach(pid); if (!tids_or) // 如果附加失败 return tids_or.takeError(); // 返回错误信息 ArrayRef<::pid_t> tids = *tids_or; // 获取成功返回的线程ID列表 // 通过进程的第一个线程ID确定目标进程的架构 llvm::Expected<ArchSpec> arch_or = NativeRegisterContextLinux::DetermineArchitecture(tids[0]); if (!arch_or) // 如果架构确定失败 return arch_or.takeError(); // 返回错误信息 // 创建并返回NativeProcessLinux实例, 封装了对目标进程的调试控制 return std::unique_ptr<NativeProcessLinux>( new NativeProcessLinux(pid, -1, native_delegate, *arch_or, *this, tids)); } // 附加到指定PID的进程, 并返回该进程中所有线程的ID列表 // 参数: pid - 要附加的进程ID // 返回值: 包含线程ID列表的Expected对象, 成功则为线程列表, 失败则为错误信息 llvm::Expected<std::vector<::pid_t>> NativeProcessLinux::Attach(::pid_t pid) { Log *log = GetLog(POSIXLog::Process); // 获取进程相关的日志对象 Status status; // 使用映射表跟踪需要附加和已附加的线程(键为线程ID, 值为是否已附加) Host::TidMap tids_to_attach; // 查找目标进程的所有线程, 填充到tids_to_attach中 while (Host::FindProcessThreads(pid, tids_to_attach)) { // 遍历所有需要处理的线程 for (Host::TidMap::iterator it = tids_to_attach.begin(); it != tids_to_attach.end();) { // 处理尚未附加的线程 if (it->second == false) { lldb::tid_t tid = it->first; // 获取当前线程ID // 附加到请求的线程, 这会导致线程收到SIGSTOP信号而停止 if ((status = PtraceWrapper(PTRACE_ATTACH, tid)).Fail()) { // 线程不存在(可能已退出), 从映射表中移除并继续处理其他线程 if (status.GetError() == ESRCH) { it = tids_to_attach.erase(it); continue; } // 没有权限附加到线程, 可能是ptrace_scope设置导致 if (status.GetError() == EPERM) { // 返回包含ptrace_scope相关提示的错误信息 return AddPtraceScopeNote(status.ToError()); } // 其他错误, 直接返回错误信息 return status.ToError(); } // 等待线程停止(使用__WALL标志确保能等待到线程) int wpid = llvm::sys::RetryAfterSignal(-1, ::waitpid, tid, nullptr, __WALL); // 如果waitpid失败(返回值<0) if (wpid < 0) { // 线程不存在, 从映射表中移除并继续 if (errno == ESRCH) { it = tids_to_attach.erase(it); continue; } // 返回系统错误信息 return llvm::errorCodeToError( std::error_code(errno, std::generic_category())); } // 为当前线程设置默认的ptrace选项 if ((status = SetDefaultPtraceOpts(tid)).Fail()) return status.ToError(); LLDB_LOG(log, "adding tid = {0}", tid); // 记录已附加的线程ID it->second = true; // 标记该线程已成功附加 } // 移动迭代器到下一个线程 ++it; } } // 检查是否成功附加到至少一个线程 size_t tid_count = tids_to_attach.size(); if (tid_count == 0) return llvm::make_error<StringError>("No such process", llvm::inconvertibleErrorCode()); // 收集所有已附加的线程ID到向量中并返回 std::vector<::pid_t> tids; tids.reserve(tid_count); // 预分配内存提升效率 for (const auto &p : tids_to_attach) tids.push_back(p.first); return std::move(tids); } ``` 附加一个进程要做的是就是根据这个进程 pid 找到它所有的线程, 然后 `PtraceWrapper(PTRACE_ATTACH, tid)` 用 ptrace 向所有的线程发送 `PTRACE_ATTACH` 命令, 此时这个进程会进入被调试状态, 内核向被调试进程中的每个线程发送一个 `SIGSTOP` 信号使其暂停, 处于 PTRACED 状态, 而调用 ptrace 的进程, 即调试进程等待 `SIGCHLD` 信号. 再看一下 `FindProcessThreads` 方法: ```cpp /// @brief 查找指定进程(PID)下的所有线程, 并更新线程ID映射表 /// @param[in] pid 目标进程的ID, 需查找该进程下的所有线程 /// @param[inout] tids_to_attach 线程ID映射表(键:线程ID, 值:是否已附加), /// 函数会将新发现的线程ID插入表中, 标记为“未附加”(false) /// @return 若本次查找发现了新线程(映射表有更新)则返回true, 否则返回false bool Host::FindProcessThreads(const lldb::pid_t pid, TidMap &tids_to_attach) { // 标记映射表是否有变化(是否新增了线程) bool tids_changed = false; // 静态常量:/proc/ 是Linux系统中用于查看进程信息的虚拟文件系统根路径 static const char procdir[] = "/proc/"; // 静态常量:/task/ 是每个进程目录下存储线程信息的子目录名(每个线程对应一个子目录) static const char taskdir[] = "/task/"; // 拼接目标进程的线程目录路径:/proc/[pid]/task/ // 例如pid=1234时, 路径为/proc/1234/task/, 该目录下的子目录名即为线程ID(TID) std::string process_task_dir = procdir + llvm::to_string(pid) + taskdir; // 打开目标进程的线程目录, 获取目录句柄 DIR *dirproc = opendir(process_task_dir.c_str()); // 若成功打开线程目录(说明进程存在且有权限访问) if (dirproc) { // 目录项结构体, 用于存储每次读取到的目录内容(如子目录名、文件类型) struct dirent *direntry = nullptr; // 循环读取目录中的每一项内容, 直到读取完毕(direntry为nullptr) while ((direntry = readdir(dirproc)) != nullptr) { // 过滤非目录项 + 非数字命名的目录: // 1. direntry->d_type != DT_DIR:排除文件(线程目录下只有子目录, 每个子目录对应一个线程) // 2. !IsDirNumeric(direntry->d_name):排除非数字命名的目录(如.和.., 表示当前/上级目录) if (direntry->d_type != DT_DIR || !IsDirNumeric(direntry->d_name)) continue; // 将线程目录名(字符串)转换为整数型线程ID(TID) lldb::tid_t tid = atoi(direntry->d_name); // 在映射表中查找当前TID, 判断是否已存在 TidMap::iterator it = tids_to_attach.find(tid); // 若TID不存在于映射表中(发现新线程) if (it == tids_to_attach.end()) { // 将新线程ID插入映射表, 标记为“未附加”(false) tids_to_attach.insert(TidPair(tid, false)); // 标记映射表发生变化(新增了线程) tids_changed = true; } } // 关闭目录句柄, 释放资源(避免内存泄漏) closedir(dirproc); } // 返回映射表是否有更新(是否发现新线程) return tids_changed; } ``` 相当于这种:  读取 `/proc/[pid]/task/` 虚拟文件中的信息, 找到对应 pid 的 tid. #### Halt ```cpp Status NativeProcessLinux::Halt() { Status error; // 用于存储操作结果状态 // 向当前进程(GetID()返回进程ID)发送SIGSTOP信号 // SIGSTOP是一个不能被忽略或捕获的信号, 会强制进程暂停执行 if (kill(GetID(), SIGSTOP) != 0) { // 若信号发送失败(返回非0), 将系统错误码(errno)转换为Status错误信息 error.SetErrorToErrno(); } // 返回操作结果:成功时error为空, 失败时包含具体错误 return error; } ``` 通过向被调试进程发送 `SIGSTOP` 信号来强制进程暂停, 是调试过程中中断程序运行的常用手段. 调试器后续可以通过 `Resume` 方法恢复进程执行. #### Resume ```cpp /// \param[in] resume_actions 包含每个线程的恢复动作列表(状态和信号) /// \return 成功返回空状态, 失败返回错误信息 Status NativeProcessLinux::Resume(const ResumeActionList &resume_actions) { // 获取日志对象, 用于调试日志输出(POSIX 进程相关日志) Log *log = GetLog(POSIXLog::Process); // 记录当前进程 ID 到日志 LLDB_LOG(log, "pid {0}", GetID()); // 通知跟踪器(如 Intel PT)进程即将恢复执行 NotifyTracersProcessWillResume(); // 检查当前平台是否支持硬件单步执行, 不支持则使用软件单步 bool software_single_step = !SupportHardwareSingleStepping(); // 如果需要使用软件单步执行 if (software_single_step) { // 遍历所有线程, 为需要单步的线程设置软件单步环境 for (const auto &thread : m_threads) { assert(thread && "thread list should not contain NULL threads"); // 从恢复动作列表中获取当前线程对应的动作(若不存在则用默认动作) const ResumeAction *const action = resume_actions.GetActionForThread(thread->GetID(), true); if (action == nullptr) continue; // 如果线程需要单步执行(eStateStepping) if (action->state == eStateStepping) { // 为线程设置软件单步(如修改指令指针、插入断点等) Status error = SetupSoftwareSingleStepping( static_cast<NativeThreadLinux &>(*thread)); if (error.Fail()) return error; // 若设置失败, 直接返回错误 } } } // 遍历所有线程, 执行对应的恢复动作 for (const auto &thread : m_threads) { assert(thread && "thread list should not contain NULL threads"); // 获取当前线程的恢复动作 const ResumeAction *const action = resume_actions.GetActionForThread(thread->GetID(), true); // 若没有为该线程指定动作, 跳过(保持当前状态) if (action == nullptr) { LLDB_LOG(log, "no action specified for pid {0} tid {1}", GetID(), thread->GetID()); continue; } // 记录当前处理的线程及其恢复状态 LLDB_LOG(log, "processing resume action state {0} for pid {1} tid {2}", action->state, GetID(), thread->GetID()); // 根据恢复动作的状态(如运行、单步、暂停)执行不同操作 switch (action->state) { case eStateRunning: // 继续运行线程 case eStateStepping: { // 单步执行线程 // 获取需要传递给线程的信号(如断点信号需要在恢复时处理) const int signo = action->signal; // 调用 ResumeThread 实际恢复线程执行, 传入目标状态和信号 Status error = ResumeThread(static_cast<NativeThreadLinux &>(*thread), action->state, signo); if (error.Fail()) { // 若恢复失败, 返回包含详细信息的错误 return Status("NativeProcessLinux::%s: failed to resume thread " "for pid %" PRIu64 ", tid %" PRIu64 ", error = %s", __FUNCTION__, GetID(), thread->GetID(), error.AsCString()); } break; } case eStateSuspended: // 线程保持暂停状态 case eStateStopped: // 线程保持停止状态 // 无需操作, 线程继续保持当前状态 break; default: // 处理未知状态, 返回错误 return Status("NativeProcessLinux::%s (): unexpected state %s specified " "for pid %" PRIu64 ", tid %" PRIu64, __FUNCTION__, StateAsCString(action->state), GetID(), thread->GetID()); } } // 所有线程的恢复动作处理完成, 返回成功状态 return Status(); } ``` 恢复被调试进程中线程的执行, 根据 resume_actions 中指定的动作控制每个线程的行为, 如继续运行、单步执行等. 其中有几个值得细看的方法, 让我们一一探究一下. 首先是 `SupportHardwareSingleStepping`: #### SupportHardwareSingleStepping ```cpp /// 判断当前架构是否支持硬件单步执行 /// 硬件单步执行通常通过CPU的调试寄存器实现, 比软件单步更高效 /// \return 若支持硬件单步返回true, 否则返回false bool NativeProcessLinux::SupportHardwareSingleStepping() const { // 判断当前进程的架构是否为以下不支持硬件单步的架构: // - MIPS架构 // - ARM架构(llvm::Triple::arm对应传统ARM架构) // - RISC-V架构 // - 龙芯架构(LoongArch) if (m_arch.IsMIPS() || m_arch.GetMachine() == llvm::Triple::arm || m_arch.GetTriple().isRISCV() || m_arch.GetTriple().isLoongArch()) return false; // 上述架构不支持硬件单步 // 其他架构(如x86、x86_64、AArch64等)默认支持硬件单步 return true; } ``` 根据机构类型确定是否支持硬件断点, 硬件断点需要硬件支持(这个像是废话), 后面会看如何实现硬件/软件断点. 这个架构类型信息 `m_arch` 是在 `Launch` / `Attach` 中确定的, 具体来说是在编译时通过 cmake 确定的. 可以看一下 llvm 支持的所有架构: ```cpp enum ArchType { UnknownArch, ///< 未知架构 arm, ///< ARM架构(小端模式):包括arm、armv.*、xscale等系列 armeb, ///< ARM架构(大端模式):armeb aarch64, ///< AArch64架构(小端模式):64位ARM架构 aarch64_be, ///< AArch64架构(大端模式) aarch64_32, ///< AArch64架构(小端模式, ILP32数据模型):32位应用运行在64位AArch64上 arc, ///< ARC架构:Synopsys公司的ARC处理器 avr, ///< AVR架构:Atmel公司的AVR微控制器 bpfel, ///< eBPF/扩展BPF(小端模式):64位BPF虚拟机 bpfeb, ///< eBPF/扩展BPF(大端模式) csky, ///< CSKY架构:中国平头哥的玄铁处理器 dxil, ///< DXIL:32位DirectX字节码架构 hexagon, ///< Hexagon架构:高通公司的Hexagon处理器 loongarch32, ///< 龙芯架构(32位):loongarch32 loongarch64, ///< 龙芯架构(64位):loongarch64 m68k, ///< M68k架构:摩托罗拉680x0系列处理器 mips, ///< MIPS架构(大端):包括mips、mipsallegrex、mipsr6等 mipsel, ///< MIPS架构(小端):mipsel、mipsallegrexe、mipsr6el等 mips64, ///< MIPS64架构(大端):mips64、mips64r6、mipsn32等 mips64el, ///< MIPS64架构(小端):mips64el、mips64r6el等 msp430, ///< MSP430架构:德州仪器的MSP430微控制器 ppc, ///< PPC架构:PowerPC(32位, 大端) ppcle, ///< PPCLE架构:PowerPC(32位, 小端) ppc64, ///< PPC64架构:64位PowerPC(大端), 包括ppu ppc64le, ///< PPC64LE架构:64位PowerPC(小端) r600, ///< R600架构:AMD的HD2XXX到HD6XXX系列GPU amdgcn, ///< AMDGCN架构:AMD的GCN系列GPU riscv32, ///< RISC-V架构(32位) riscv64, ///< RISC-V架构(64位) sparc, ///< Sparc架构(32位, 大端) sparcv9, ///< Sparcv9架构(64位Sparc) sparcel, ///< Sparc架构(小端), 注意与Sparcle CPU变体区分 systemz, ///< SystemZ架构:IBM的s390x大型机架构 tce, ///< TCE架构:基于Transport Triggered Architecture的处理器 tcele, ///< TCE架构(小端模式) thumb, ///< Thumb架构(小端):ARM的Thumb指令集, 包括thumbv.*系列 thumbeb, ///< Thumb架构(大端) x86, ///< X86架构:32位x86处理器, 如i386到i986 x86_64, ///< X86-64架构:64位x86处理器, 如amd64、x86_64 xcore, ///< XCore架构:XMOS公司的多核处理器 xtensa, ///< Xtensa架构:Tensilica公司的可配置处理器 nvptx, ///< NVPTX架构:NVIDIA GPU的32位虚拟指令集 nvptx64, ///< NVPTX64架构:NVIDIA GPU的64位虚拟指令集 le32, ///< le32架构:通用32位小端CPU(如PNaCl) le64, ///< le64架构:通用64位小端CPU(如PNaCl) amdil, ///< AMDIL架构:AMD的中间语言(32位) amdil64, ///< AMDIL64架构:64位指针的AMDIL hsail, ///< HSAIL架构:AMD的异构系统架构中间语言(32位) hsail64, ///< HSAIL64架构:64位指针的HSAIL spir, ///< SPIR架构:OpenCL的32位标准可移植中间表示 spir64, ///< SPIR64架构:OpenCL的64位标准可移植中间表示 spirv32, ///< SPIR-V架构(32位指针):Vulkan/OpenCL的中间语言 spirv64, ///< SPIR-V架构(64位指针) kalimba, ///< Kalimba架构:通用Kalimba处理器(常用于音频处理) shave, ///< SHAVE架构:Movidius的向量VLIW处理器 lanai, ///< Lanai架构:32位Lanai处理器 wasm32, ///< WebAssembly架构(32位指针) wasm64, ///< WebAssembly架构(64位指针) renderscript32, ///< 32位RenderScript:Android的并行计算框架 renderscript64, ///< 64位RenderScript ve, ///< VE架构:NEC的SX-Aurora向量引擎 LastArchType = ve ///< 枚举值的结束标记, 用于边界检查 }; ``` 再来看一下 `SetupSoftwareSingleStepping`: #### SetupSoftwareSingleStepping ```cpp /// \param[in] thread 需要设置软件单步的线程 /// \return 成功返回空状态, 失败返回包含错误信息的状态 Status NativeProcessSoftwareSingleStep::SetupSoftwareSingleStepping( NativeThreadProtocol &thread) { Status error; // 存储操作结果状态 // 获取线程所属的进程、线程的寄存器上下文、进程的架构信息 NativeProcessProtocol &process = thread.GetProcess(); NativeRegisterContext ®ister_context = thread.GetRegisterContext(); const ArchSpec &arch = process.GetArchitecture(); // 为当前架构创建指令模拟器(用于解析指令、预测下一条指令地址) // 模拟器类型指定为“修改PC的指令”(eInstructionTypePCModifying), 聚焦影响程序计数器的指令 std::unique_ptr<EmulateInstruction> emulator_up( EmulateInstruction::FindPlugin(arch, eInstructionTypePCModifying, nullptr)); // 若未找到对应架构的指令模拟器, 返回错误(无法预测下一条指令地址, 无法设置软件单步) if (emulator_up == nullptr) return Status("Instruction emulator not found!"); // 创建模拟器的“数据载体”(Baton), 用于传递进程、寄存器上下文给模拟器回调函数 EmulatorBaton baton(process, register_context); // 将Baton绑定到模拟器, 供后续回调函数访问进程/寄存器信息 emulator_up->SetBaton(&baton); // 设置模拟器的内存/寄存器读写回调函数(模拟器需通过这些回调获取真实进程的内存和寄存器数据) emulator_up->SetReadMemCallback(&ReadMemoryCallback); // 读内存回调 emulator_up->SetReadRegCallback(&ReadRegisterCallback); // 读寄存器回调 emulator_up->SetWriteMemCallback(&WriteMemoryCallback); // 写内存回调 emulator_up->SetWriteRegCallback(&WriteRegisterCallback); // 写寄存器回调 // 读取当前线程PC指向的指令(从被调试进程内存中读取) if (!emulator_up->ReadInstruction()) return Status("Read instruction failed!"); // 模拟执行当前指令, 自动更新PC(预测执行完该指令后的下一个PC值) // eEmulateInstructionOptionAutoAdvancePC:让模拟器自动计算并更新PC bool emulation_result = emulator_up->EvaluateInstruction(eEmulateInstructionOptionAutoAdvancePC); // 获取通用寄存器中“程序计数器(PC)”和“标志寄存器(Flags)”的信息 const RegisterInfo *reg_info_pc = register_context.GetRegisterInfo( eRegisterKindGeneric, LLDB_REGNUM_GENERIC_PC); // 通用PC寄存器 const RegisterInfo *reg_info_flags = register_context.GetRegisterInfo( eRegisterKindGeneric, LLDB_REGNUM_GENERIC_FLAGS); // 通用标志寄存器(如x86的EFLAGS) // 从Baton中查找模拟后PC和Flags的数值(Baton在回调中记录了模拟器修改的寄存器值) // 键值为寄存器的DWARF编号(调试标准中定义的寄存器唯一标识) auto pc_it = baton.m_register_values.find(reg_info_pc->kinds[eRegisterKindDWARF]); auto flags_it = reg_info_flags == nullptr ? baton.m_register_values.end() // 若架构无Flags寄存器, 直接设为end : baton.m_register_values.find( reg_info_flags->kinds[eRegisterKindDWARF]); lldb::addr_t next_pc; // 执行完当前指令后的下一个PC地址(软件断点要插入的位置) lldb::addr_t next_flags; // 执行完当前指令后的Flags值(用于判断架构模式, 如ARM/Thumb) // 根据指令模拟结果, 计算next_pc和next_flags if (emulation_result) { // 模拟成功:说明模拟器已正确预测下一个PC assert(pc_it != baton.m_register_values.end() && "Emulation was successfull but PC wasn't updated"); next_pc = pc_it->second.GetAsUInt64(); // 从Baton中获取模拟后的PC值 // 获取模拟后的Flags值(若存在Flags寄存器), 否则直接读取当前Flags if (flags_it != baton.m_register_values.end()) next_flags = flags_it->second.GetAsUInt64(); else next_flags = ReadFlags(register_context); } else if (pc_it == baton.m_register_values.end()) { // 模拟失败, 但未修改PC:说明当前指令不影响PC(如普通运算指令) // 此时直接用“当前PC + 指令长度”作为下一个PC(简单递增) next_pc = register_context.GetPC() + emulator_up->GetOpcode().GetByteSize(); next_flags = ReadFlags(register_context); // 直接读取当前Flags } else { // 模拟失败, 但已修改PC:属于未知错误(影响PC的指令模拟失败, 无法确定下一个PC) return Status("Instruction emulation failed unexpectedly."); } // 根据架构类型, 确定软件断点的“大小提示”(不同架构的指令长度不同, 断点需覆盖完整指令) int size_hint = 0; if (arch.GetMachine() == llvm::Triple::arm) { // ARM架构:需区分ARM模式(4字节指令)和Thumb模式(2字节指令) // Flags的第5位(0x20)表示Thumb模式(ARM架构标准) if (next_flags & 0x20) { size_hint = 2; // Thumb模式:断点大小2字节 } else { size_hint = 4; // ARM模式:断点大小4字节 } } else if (arch.IsMIPS() || arch.GetTriple().isPPC64() || arch.GetTriple().isRISCV() || arch.GetTriple().isLoongArch()) { // 这些架构的指令长度固定为4字节, 断点大小设为4 size_hint = 4; } // 在预测的next_pc地址插入软件断点(hardware=false表示软件断点) error = process.SetBreakpoint(next_pc, size_hint, /*hardware=*/false); // 特殊错误处理:若断点插入失败是因为next_pc超出地址空间(如非法地址) // 此时忽略错误, 让被调试进程触发段错误(由调试器后续处理) if (error.GetError() == EIO || error.GetError() == EFAULT) { return Status(); } else if (error.Fail()) { // 其他断点插入失败(如权限不足), 返回错误 return error; } // 记录“正在软件单步的线程”:键为线程ID, 值为下一个PC(后续断点触发后需移除该断点) m_threads_stepping_with_breakpoint.insert({thread.GetID(), next_pc}); // 软件单步设置完成 return Status(); } ``` 设置软件单步断点, 实现逻辑主要为: - 在软件层面模拟执行下一条指令, 计算出下一条指令执行完的 PC, 即 next_pc - 将 next_pc 处指令替换为软件断点指令(如 int 3) - 记录 {tid, next_pc}, 以便后面触发断点后移除, 恢复原指令. 我们按顺序看一下 `ReadMemoryCallback`, `ReadRegisterCallback`(`WriteMemoryCallback` `WriteRegisterCallback` 不看, 几乎没有内容): #### ReadMemoryCallback ```cpp static size_t ReadMemoryCallback(EmulateInstruction *instruction, void *baton, const EmulateInstruction::Context &context, lldb::addr_t addr, void *dst, size_t length) { EmulatorBaton *emulator_baton = static_cast<EmulatorBaton *>(baton); size_t bytes_read; emulator_baton->m_process.ReadMemory(addr, dst, length, bytes_read); return bytes_read; } ``` `emulator_baton->m_process.ReadMemory` 对应了 `NativeProcessLinux` 类中的 `ReadMemory` 方法: ```cpp /// \param [in] addr 要读取的内存地址 /// \param [in] buf 存储读取结果的缓冲区 /// \param [in] size 要读取的字节数 /// \param [out] bytes_read 实际读取的字节数 /// \return 成功返回空状态, 失败返回包含错误信息的状态 Status NativeProcessLinux::ReadMemory(lldb::addr_t addr, void *buf, size_t size, size_t &bytes_read) { // 检查是否支持process_vm_readv系统调用(更高效的内存读取方式) if (ProcessVmReadvSupported()) { // process_vm_readv路径比ptrace API快约50倍, 若支持则优先使用 // 定义本地和远程的iovec结构(用于描述数据缓冲区) struct iovec local_iov, remote_iov; local_iov.iov_base = buf; // 本地缓冲区地址(存储读取结果) local_iov.iov_len = size; // 本地缓冲区长度 remote_iov.iov_base = reinterpret_cast<void *>(addr); // 远程进程地址(要读取的地址) remote_iov.iov_len = size; // 要读取的长度 // 调用process_vm_readv系统调用读取内存 bytes_read = process_vm_readv(GetCurrentThreadID(), &local_iov, 1, &remote_iov, 1, 0); // 判断是否成功读取了请求的所有字节 const bool success = bytes_read == size; // 获取日志对象并记录操作信息 Log *log = GetLog(POSIXLog::Process); LLDB_LOG(log, "using process_vm_readv to read {0} bytes from inferior " "address {1:x}: {2}", size, addr, success ? "Success" : llvm::sys::StrError(errno)); // 若成功读取所有字节, 返回成功状态 if (success) return Status(); // 若失败, 则回退使用ptrace API重试 } // 以下为使用ptrace API读取内存的逻辑 unsigned char *dst = static_cast<unsigned char *>(buf); // 目标缓冲区指针 size_t remainder; // 剩余需要读取的字节数 long data; // 存储从ptrace读取的字数据 // 获取内存操作相关的日志对象 Log *log = GetLog(POSIXLog::Memory); LLDB_LOG(log, "addr = {0}, buf = {1}, size = {2}", addr, buf, size); // 循环读取所有请求的字节 for (bytes_read = 0; bytes_read < size; bytes_read += remainder) { // 调用ptrace包装函数读取一个字的数据 Status error = NativeProcessLinux::PtraceWrapper( PTRACE_PEEKDATA, GetCurrentThreadID(), (void *)addr, nullptr, 0, &data); // 若读取失败, 返回错误状态 if (error.Fail()) return error; // 计算剩余需要读取的字节数 remainder = size - bytes_read; // 每次最多读取一个字的大小(k_ptrace_word_size) remainder = remainder > k_ptrace_word_size ? k_ptrace_word_size : remainder; // 将读取到的数据复制到目标缓冲区 memcpy(dst, &data, remainder); // 记录读取的内存地址和数据 LLDB_LOG(log, "[{0:x}]:{1:x}", addr, data); // 移动地址指针和缓冲区指针到下一个字 addr += k_ptrace_word_size; dst += k_ptrace_word_size; } // 全部读取完成, 返回成功状态 return Status(); } ``` 首先尝试用 `process_vm_readv` 系统调用, 如果不支持, 使用 ptrace 的包装方法 `PtraceWrapper` 发送 `PTRACE_PEEKDATA` 命令. 我们可以再看一下 `NativeProcessLinux` 类中的 `ReadMemory` 方法: ```cpp // 向Linux系统中的目标进程内存写入数据 // 参数: // - addr: 目标内存地址(要写入数据的起始地址) // - buf: 指向源数据缓冲区的指针(待写入的数据) // - size: 要写入的数据大小(字节数) // - bytes_written: 输出参数, 实际成功写入的字节数 // 返回值:操作状态(成功或失败信息) Status NativeProcessLinux::WriteMemory(lldb::addr_t addr, const void *buf, size_t size, size_t &bytes_written) { // 将源数据缓冲区指针转换为unsigned char*, 方便按字节操作 const unsigned char *src = static_cast<const unsigned char *>(buf); size_t remainder; // 记录每次循环中待写入的剩余字节数 Status error; // 用于存储操作状态 // 获取日志对象, 用于记录内存操作相关日志 Log *log = GetLog(POSIXLog::Memory); // 记录写入操作的起始信息(地址、缓冲区、大小) LLDB_LOG(log, "addr = {0}, buf = {1}, size = {2}", addr, buf, size); // 循环写入数据, 直到所有数据都被写入或发生错误 // bytes_written记录已写入的总字节数, 初始为0 for (bytes_written = 0; bytes_written < size; bytes_written += remainder) { // 计算当前剩余待写入的字节数 remainder = size - bytes_written; // 每次写入的最大字节数不超过ptrace操作的字长(k_ptrace_word_size) remainder = remainder > k_ptrace_word_size ? k_ptrace_word_size : remainder; // 当剩余字节数等于ptrace字长时, 直接进行完整字写入 if (remainder == k_ptrace_word_size) { unsigned long data = 0; // 存储要写入的字数据 // 将源缓冲区中的数据复制到data变量(按ptrace字长) memcpy(&data, src, k_ptrace_word_size); // 记录当前写入的地址和数据(十六进制) LLDB_LOG(log, "[{0:x}]:{1:x}", addr, data); // 调用ptrace的PTRACE_POKEDATA命令写入数据 // 参数:命令类型、目标线程ID、目标地址、要写入的数据 error = NativeProcessLinux::PtraceWrapper( PTRACE_POKEDATA, GetCurrentThreadID(), (void *)addr, (void *)data); // 如果写入失败, 返回错误状态 if (error.Fail()) return error; } else { // 当剩余字节数小于ptrace字长时, 需要先读取原有数据再部分修改 unsigned char buff[8]; // 临时缓冲区, 用于存储读取到的原有数据 size_t bytes_read; // 实际读取的字节数 // 先读取目标地址处的完整ptrace字长数据 error = ReadMemory(addr, buff, k_ptrace_word_size, bytes_read); if (error.Fail()) return error; // 将源数据中剩余的部分复制到临时缓冲区(覆盖原有数据的对应部分) memcpy(buff, src, remainder); // 将修改后的完整字数据写回目标地址 size_t bytes_written_rec; error = WriteMemory(addr, buff, k_ptrace_word_size, bytes_written_rec); if (error.Fail()) return error; // 记录写入的地址、源数据和实际写入的数据(用于调试) LLDB_LOG(log, "[{0:x}]:{1:x} ({2:x})", addr, *(const unsigned long *)src, *(unsigned long *)buff); } // 移动地址指针和源数据指针, 准备下一次循环 addr += k_ptrace_word_size; src += k_ptrace_word_size; } // 所有数据写入完成, 返回成功状态 return error; } ``` 可以看到还是依赖的 ptrace 的包装方法 `PtraceWrapper` 发送 `PTRACE_POKEDATA` 命令. #### ReadRegisterCallback ```cpp // 读取寄存器回调函数, 用于在指令模拟过程中获取寄存器值 // 参数说明: // - instruction: 正在模拟的指令对象 // - baton: 传递的参数, 这里是EmulatorBaton类型的指针 // - reg_info: 要读取的寄存器信息 // - reg_value: 输出参数, 用于存储读取到的寄存器值 // 返回值:成功读取寄存器值返回true, 否则返回false static bool ReadRegisterCallback(EmulateInstruction *instruction, void *baton, const RegisterInfo *reg_info, RegisterValue ®_value) { // 将传递的通用指针转换为EmulatorBaton类型指针, 以便访问其中的数据 EmulatorBaton *emulator_baton = static_cast<EmulatorBaton *>(baton); // 在寄存器值映射表中查找指定DWARF类型寄存器的值 // 使用DWARF寄存器编号作为键进行查找 auto it = emulator_baton->m_register_values.find( reg_info->kinds[eRegisterKindDWARF]); // 如果找到对应的寄存器值 if (it != emulator_baton->m_register_values.end()) { // 将找到的值赋给输出参数 reg_value = it->second; // 成功获取寄存器值, 返回true return true; } // 如果在映射表中未找到, 说明需要从寄存器上下文中获取 // 模拟器仅填充DWARF寄存器编号(在某些情况下还有通用寄存器编号) // 基于DWARF寄存器编号从寄存器上下文获取完整的寄存器信息 const RegisterInfo *full_reg_info = emulator_baton->m_reg_context.GetRegisterInfo( eRegisterKindDWARF, reg_info->kinds[eRegisterKindDWARF]); // 从寄存器上下文读取寄存器值 Status error = emulator_baton->m_reg_context.ReadRegister(full_reg_info, reg_value); // 如果读取成功, 返回true if (error.Success()) return true; // 所有尝试都失败, 返回false return false; } ``` `emulator_baton->m_reg_context.ReadRegister` 对应了 `NativeRegisterContext` 类中的 `ReadRegister` 虚方法, 对于不同的架构有不同的实现, 这里我们看 `lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm64.cpp`: ```cpp // 读取ARM64架构下指定寄存器的值 // 参数: // - reg_info: 寄存器信息结构体, 包含寄存器的各种属性(如编号、偏移量等) // - reg_value: 输出参数, 用于存储读取到的寄存器值 // 返回值:操作状态(成功或失败信息) Status NativeRegisterContextLinux_arm64::ReadRegister(const RegisterInfo *reg_info, RegisterValue ®_value) { Status error; // 用于存储操作状态的变量 // 检查寄存器信息指针是否为空 if (!reg_info) { error.SetErrorString("reg_info NULL"); // 设置错误信息 return error; } // 获取LLDB内部使用的寄存器编号(不同于硬件或DWARF编号) const uint32_t reg = reg_info->kinds[lldb::eRegisterKindLLDB]; // 检查寄存器编号是否有效 if (reg == LLDB_INVALID_REGNUM) return Status("no lldb regnum for %s", reg_info && reg_info->name ? reg_info->name : "<unknown register>"); // 声明用于存储数据来源的指针和相关变量 uint8_t *src; // 指向寄存器数据的源地址 uint32_t offset = LLDB_INVALID_INDEX32; // 寄存器在缓冲区中的偏移量 uint64_t sve_vg; // SVE向量长度相关值 std::vector<uint8_t> sve_reg_non_live; // 用于存储非活跃SVE寄存器的临时缓冲区 // 处理通用寄存器(GPR: General Purpose Registers) if (IsGPR(reg)) { error = ReadGPR(); // 读取所有通用寄存器到内部缓冲区 if (error.Fail()) // 检查读取是否失败 return error; // 获取该寄存器在GPR缓冲区中的字节偏移量 offset = reg_info->byte_offset; // 断言确保偏移量在有效范围内(调试时检查用) assert(offset < GetGPRSize()); // 计算源数据地址 = GPR缓冲区起始地址 + 偏移量 src = (uint8_t *)GetGPRBuffer() + offset; // 处理浮点寄存器(FPR: Floating Point Registers) } else if (IsFPR(reg)) { // 检查SVE(Scalable Vector Extension)是否禁用 if (m_sve_state == SVEState::Disabled) { // SVE禁用时, 使用传统方式访问FPU寄存器 error = ReadFPR(); // 读取所有浮点寄存器到内部缓冲区 if (error.Fail()) return error; // 计算该浮点寄存器在FPR缓冲区中的偏移量 offset = CalculateFprOffset(reg_info); assert(offset < GetFPRSize()); src = (uint8_t *)GetFPRBuffer() + offset; } else { // SVE启用时, 读取并缓存SVE相关的ptrace数据 error = ReadAllSVE(); if (error.Fail()) return error; // FPSR(浮点状态寄存器)和FPCR(浮点控制寄存器)的位置因SVE状态而异: // - 在SVEState::FPSIMD状态下, 位于Z寄存器之后 // - 在SVEState::Full状态下, 位于寄存器数据末尾(需根据向量长度对齐) uint32_t sve_reg_num = LLDB_INVALID_REGNUM; // 处理FPSR寄存器 if (reg == GetRegisterInfo().GetRegNumFPSR()) { sve_reg_num = reg; if (m_sve_state == SVEState::Full) // 计算Full状态下FPSR的偏移量(基于向量长度) offset = sve::PTraceFPSROffset(sve::vq_from_vl(m_sve_header.vl)); else if (m_sve_state == SVEState::FPSIMD) // FPSIMD状态下FPSR的固定偏移量(32个16字节向量后) offset = sve::ptrace_fpsimd_offset + (32 * 16); // 处理FPCR寄存器 } else if (reg == GetRegisterInfo().GetRegNumFPCR()) { sve_reg_num = reg; if (m_sve_state == SVEState::Full) // 计算Full状态下FPCR的偏移量 offset = sve::PTraceFPCROffset(sve::vq_from_vl(m_sve_header.vl)); else if (m_sve_state == SVEState::FPSIMD) // FPSIMD状态下FPCR的固定偏移量(FPSR后4字节) offset = sve::ptrace_fpsimd_offset + (32 * 16) + 4; // 处理其他浮点寄存器 } else { // 从寄存器信息中提取对应的SVE Z寄存器编号 if (reg_info->value_regs && reg_info->value_regs[0] != LLDB_INVALID_REGNUM) sve_reg_num = reg_info->value_regs[0]; // 计算SVE寄存器在缓冲区中的偏移量 offset = CalculateSVEOffset(GetRegisterInfoAtIndex(sve_reg_num)); } assert(offset < GetSVEBufferSize()); src = (uint8_t *)GetSVEBuffer() + offset; } // 处理TLS(Thread Local Storage, 线程本地存储)相关寄存器 } else if (IsTLS(reg)) { error = ReadTLSTPIDR(); // 读取TLS相关的TPIDR寄存器 if (error.Fail()) return error; // 计算TLS寄存器在缓冲区中的偏移量(相对于TLS基地址) offset = reg_info->byte_offset - GetRegisterInfo().GetTLSOffset(); assert(offset < GetTLSTPIDRSize()); src = (uint8_t *)GetTLSTPIDR() + offset; // 处理SVE(Scalable Vector Extension, 可扩展向量扩展)寄存器 } else if (IsSVE(reg)) { // 检查SVE是否禁用或不支持 if (m_sve_state == SVEState::Disabled || m_sve_state == SVEState::Unknown) return Status("SVE disabled or not supported"); // 处理SVE向量长度寄存器(VG) if (GetRegisterInfo().IsSVERegVG(reg)) { sve_vg = GetSVERegVG(); // 获取向量长度值 src = (uint8_t *)&sve_vg; // 源地址指向该值 } else { // 读取并缓存SVE相关的ptrace数据 error = ReadAllSVE(); if (error.Fail()) return error; // 在FPSIMD状态下, SVE寄存器数据与传统fpsimd结构体兼容 if (m_sve_state == SVEState::FPSIMD) { // 初始化临时缓冲区, 用0填充(非活跃部分置0) sve_reg_non_live.resize(reg_info->byte_size, 0); src = sve_reg_non_live.data(); // 如果是SVE Z寄存器, 复制低16字节(与传统V寄存器兼容部分) if (GetRegisterInfo().IsSVEZReg(reg)) { offset = CalculateSVEOffset(reg_info); assert(offset < GetSVEBufferSize()); ::memcpy(sve_reg_non_live.data(), (uint8_t *)GetSVEBuffer() + offset, 16); } } else { // Full状态下直接从SVE缓冲区读取 offset = CalculateSVEOffset(reg_info); assert(offset < GetSVEBufferSize()); src = (uint8_t *)GetSVEBuffer() + offset; } } // 处理PAuth(Pointer Authentication, 指针认证)相关寄存器 } else if (IsPAuth(reg)) { error = ReadPAuthMask(); // 读取PAuth掩码寄存器 if (error.Fail()) return error; // 计算PAuth寄存器在缓冲区中的偏移量 offset = reg_info->byte_offset - GetRegisterInfo().GetPAuthOffset(); assert(offset < GetPACMaskSize()); src = (uint8_t *)GetPACMask() + offset; // 处理MTE(Memory Tagging Extension, 内存标签扩展)相关寄存器 } else if (IsMTE(reg)) { error = ReadMTEControl(); // 读取MTE控制寄存器 if (error.Fail()) return error; // 计算MTE寄存器在缓冲区中的偏移量 offset = reg_info->byte_offset - GetRegisterInfo().GetMTEOffset(); assert(offset < GetMTEControlSize()); src = (uint8_t *)GetMTEControl() + offset; // 未识别的寄存器类型 } else return Status("failed - register wasn't recognized to be a GPR or an FPR, " "write strategy unknown"); // 将内存中的原始数据转换为RegisterValue格式(考虑字节序等) reg_value.SetFromMemoryData(*reg_info, src, reg_info->byte_size, eByteOrderLittle, error); return error; // 返回最终操作状态 } ``` ReadGPR ReadFPR ReadAllSVE...实现上都差不多: ```cpp Status NativeRegisterContextLinux_arm64::ReadGPR() { Status error; if (m_gpr_is_valid) return error; struct iovec ioVec; ioVec.iov_base = GetGPRBuffer(); ioVec.iov_len = GetGPRBufferSize(); error = ReadRegisterSet(&ioVec, GetGPRBufferSize(), NT_PRSTATUS); if (error.Success()) m_gpr_is_valid = true; return error; } Status NativeRegisterContextLinux_arm64::ReadFPR() { Status error; if (m_fpu_is_valid) return error; struct iovec ioVec; ioVec.iov_base = GetFPRBuffer(); ioVec.iov_len = GetFPRSize(); error = ReadRegisterSet(&ioVec, GetFPRSize(), NT_FPREGSET); if (error.Success()) m_fpu_is_valid = true; return error; } Status NativeRegisterContextLinux_arm64::ReadAllSVE() { Status error; if (m_sve_buffer_is_valid) return error; struct iovec ioVec; ioVec.iov_base = GetSVEBuffer(); ioVec.iov_len = GetSVEBufferSize(); error = ReadRegisterSet(&ioVec, GetSVEBufferSize(), NT_ARM_SVE); if (error.Success()) m_sve_buffer_is_valid = true; return error; } ``` 都是调用 `ReadRegisterSet`: ```cpp Status NativeRegisterContextLinux::ReadRegisterSet(void *buf, size_t buf_size, unsigned int regset) { return NativeProcessLinux::PtraceWrapper(PTRACE_GETREGSET, m_thread.GetID(), static_cast<void *>(®set), buf, buf_size); } ``` 可以看到是调用 ptrace 的包装方法 `PtraceWrapper` 发送 `PTRACE_GETREGSET` 命令. 同样的在 `lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux.cpp` 目录中有这样一些列方法: ```cpp Status NativeRegisterContextLinux::ReadGPR() { return NativeProcessLinux::PtraceWrapper( PTRACE_GETREGS, m_thread.GetID(), nullptr, GetGPRBuffer(), GetGPRSize()); } Status NativeRegisterContextLinux::WriteGPR() { return NativeProcessLinux::PtraceWrapper( PTRACE_SETREGS, m_thread.GetID(), nullptr, GetGPRBuffer(), GetGPRSize()); } Status NativeRegisterContextLinux::ReadFPR() { return NativeProcessLinux::PtraceWrapper(PTRACE_GETFPREGS, m_thread.GetID(), nullptr, GetFPRBuffer(), GetFPRSize()); } Status NativeRegisterContextLinux::WriteFPR() { return NativeProcessLinux::PtraceWrapper(PTRACE_SETFPREGS, m_thread.GetID(), nullptr, GetFPRBuffer(), GetFPRSize()); } Status NativeRegisterContextLinux::ReadRegisterSet(void *buf, size_t buf_size, unsigned int regset) { return NativeProcessLinux::PtraceWrapper(PTRACE_GETREGSET, m_thread.GetID(), static_cast<void *>(®set), buf, buf_size); } Status NativeRegisterContextLinux::WriteRegisterSet(void *buf, size_t buf_size, unsigned int regset) { return NativeProcessLinux::PtraceWrapper(PTRACE_SETREGSET, m_thread.GetID(), static_cast<void *>(®set), buf, buf_size); } ``` 可以预料的是, 写入寄存器也是依赖 `PtraceWrapper` 发送命令. 总结来看 `读/写` -> `内存/寄存器`, 本质上都是依赖 ptrace 提供的命令, 然后做一些错误, 日志, 拷贝...处理. 回到 `SetupSoftwareSingleStepping` 跟入 `emulator_up->EvaluateInstruction`: #### EvaluateInstruction ```cpp // 评估并执行单条ARM64指令的模拟逻辑 // 参数: // - evaluate_options: 指令评估选项(控制模拟行为, 如是否自动推进PC寄存器) // 返回值:bool, true表示指令评估/模拟成功, false表示失败 bool EmulateInstructionARM64::EvaluateInstruction(uint32_t evaluate_options) { // 1. 获取当前要模拟的32位ARM64指令 opcode(机器码) const uint32_t opcode = m_opcode.GetOpcode32(); // 2. 根据opcode查找对应的指令解析数据(包含指令处理回调函数等信息) Opcode *opcode_data = GetOpcodeForInstruction(opcode); // 若未找到匹配的指令解析数据, 说明不支持该指令, 返回失败 if (opcode_data == nullptr) return false; // 3. 解析评估选项, 确定模拟行为 // - auto_advance_pc:是否自动推进程序计数器(PC) const bool auto_advance_pc = evaluate_options & eEmulateInstructionOptionAutoAdvancePC; // - m_ignore_conditions:是否忽略指令的条件执行属性(如BEQ、BNE等条件) m_ignore_conditions = evaluate_options & eEmulateInstructionOptionIgnoreConditions; // 用于标记指令模拟是否成功的变量 bool success = false; // 4. 条件执行检查(仅在不忽略条件时生效) // 若未忽略条件且当前条件不满足(success为false), 直接返回失败 // 注:此处逻辑为前置判断, 确保不满足条件时不执行后续指令模拟 if (!success && !m_ignore_conditions) return false; // 5. 处理自动推进PC的前置准备(记录当前PC值) uint32_t orig_pc_value = 0; // 存储模拟前的PC初始值 if (auto_advance_pc) { // 读取当前PC寄存器的值(使用LLDB内部寄存器编号体系, gpr_pc_arm64为ARM64 PC寄存器标识) orig_pc_value = ReadRegisterUnsigned(eRegisterKindLLDB, gpr_pc_arm64, 0, &success); // 若读取PC失败, 返回模拟失败 if (!success) return false; } // 6. 执行指令的核心模拟逻辑 // 通过指令解析数据中的回调函数(opcode_data->callback)调用对应的指令处理方法 // 传入opcode作为参数, 让回调函数处理具体指令逻辑 success = (this->*opcode_data->callback)(opcode); // 若指令核心模拟失败, 返回false if (!success) return false; // 7. 处理自动推进PC的后续逻辑 if (auto_advance_pc) { // 读取指令模拟后的PC值 uint32_t new_pc_value = ReadRegisterUnsigned(eRegisterKindLLDB, gpr_pc_arm64, 0, &success); if (!success) return false; // 若模拟后PC未发生变化(说明指令未主动修改PC, 如普通算术指令) // 则手动将PC推进4字节(ARM64指令默认占4字节) if (new_pc_value == orig_pc_value) { // 创建PC推进的上下文(标记操作类型为"推进PC") EmulateInstruction::Context context; context.type = eContextAdvancePC; context.SetNoArgs(); // 该上下文无需额外参数 // 写入新的PC值(原PC值 + 4), 若写入失败返回false if (!WriteRegisterUnsigned(context, eRegisterKindLLDB, gpr_pc_arm64, orig_pc_value + 4)) return false; } } // 8. 所有步骤执行完成, 返回指令模拟成功 return true; } ``` 根据不同的 opcode 调用对应的指令处理方法, 在软件层面解析并模拟执行指令, 而不依赖实际的硬件处理器执行, 有一个很长的对应表:  然后得到计算出新 PC 值. 这个模拟解释执行指令这里涉及的东西比较多, 并且与 ptrace 关系不大, 这个不在详细看了, 感兴趣可以自己根根看. 在回到 `SetupSoftwareSingleStepping` 跟入 `process.SetBreakpoint`, 实际是 `NativeProcessLinux` 类的 `SetBreakpoint` 方法: #### SetBreakpoint ```cpp Status NativeProcessLinux::SetBreakpoint(lldb::addr_t addr, uint32_t size, bool hardware) { if (hardware) return SetHardwareBreakpoint(addr, size); else return SetSoftwareBreakpoint(addr, size); } ``` 封装了 `NativeProcessLinux` 类 `SetHardwareBreakpoint` 与 `SetSoftwareBreakpoint` 方法, 我们一一分析一下: #### SetSoftwareBreakpoint ```cpp // 设置软件断点 // 参数: // addr: 要设置断点的内存地址 // size_hint: 断点指令的大小提示 // 返回值:操作状态(成功或失败信息) Status NativeProcessProtocol::SetSoftwareBreakpoint(lldb::addr_t addr, uint32_t size_hint) { // 获取断点相关的日志对象 Log *log = GetLog(LLDBLog::Breakpoints); // 记录断点地址和大小提示的日志 LLDB_LOG(log, "addr = {0:x}, size_hint = {1}", addr, size_hint); // 在软件断点集合中查找指定地址的断点 auto it = m_software_breakpoints.find(addr); // 如果该地址已存在断点 if (it != m_software_breakpoints.end()) { // 增加该断点的引用计数 ++it->second.ref_count; // 返回成功状态 return Status(); } // 如果不存在, 则尝试启用该地址的软件断点 auto expected_bkpt = EnableSoftwareBreakpoint(addr, size_hint); // 如果启用失败, 返回错误状态 if (!expected_bkpt) return Status(expected_bkpt.takeError()); // 将成功启用的断点添加到软件断点集合中 m_software_breakpoints.emplace(addr, std::move(*expected_bkpt)); // 返回成功状态 return Status(); } ``` 跟入 `EnableSoftwareBreakpoint`: ```cpp // 启用软件断点的具体实现 // 参数: // addr: 断点所在的内存地址 // size_hint: 提示断点指令的大小 // 返回值:成功时返回SoftwareBreakpoint对象, 失败时返回错误信息 llvm::Expected<NativeProcessProtocol::SoftwareBreakpoint> NativeProcessProtocol::EnableSoftwareBreakpoint(lldb::addr_t addr, uint32_t size_hint) { // 获取断点相关的日志记录器 Log *log = GetLog(LLDBLog::Breakpoints); // 获取适合当前架构的软件断点陷阱指令(用于触发中断) auto expected_trap = GetSoftwareBreakpointTrapOpcode(size_hint); // 如果获取陷阱指令失败, 返回错误 if (!expected_trap) return expected_trap.takeError(); // 创建缓冲区用于保存原始指令字节, 大小与陷阱指令相同 llvm::SmallVector<uint8_t, 4> saved_opcode_bytes(expected_trap->size(), 0); // 读取指定地址的原始指令字节, 以便后续恢复 size_t bytes_read = 0; Status error = ReadMemory(addr, saved_opcode_bytes.data(), saved_opcode_bytes.size(), bytes_read); // 如果读取内存失败, 返回错误 if (error.Fail()) return error.ToError(); // 确保读取的字节数与预期一致 if (bytes_read != saved_opcode_bytes.size()) { return llvm::createStringError( llvm::inconvertibleErrorCode(), "设置断点时读取内存失败:尝试读取 {0} 字节, 但实际只读取了 {1} 字节.", saved_opcode_bytes.size(), bytes_read); } // 记录将要被覆盖的原始指令字节(十六进制形式) LLDB_LOG( log, "覆盖地址 {0:x} 处的字节:{1:@[x]}", addr, llvm::make_range(saved_opcode_bytes.begin(), saved_opcode_bytes.end())); // 将陷阱指令写入目标地址, 替换原始指令以设置断点 size_t bytes_written = 0; error = WriteMemory(addr, expected_trap->data(), expected_trap->size(), bytes_written); // 如果写入内存失败, 返回错误 if (error.Fail()) return error.ToError(); // 确保写入的字节数与预期一致 if (bytes_written != expected_trap->size()) { return llvm::createStringError( llvm::inconvertibleErrorCode(), "设置断点时写入内存失败:尝试写入 {0} 字节, 但实际只写入了 {1} 字节", expected_trap->size(), bytes_written); } // 创建缓冲区用于验证写入的陷阱指令 llvm::SmallVector<uint8_t, 4> verify_bp_opcode_bytes(expected_trap->size(), 0); size_t verify_bytes_read = 0; // 重新读取目标地址的指令, 用于验证 error = ReadMemory(addr, verify_bp_opcode_bytes.data(), verify_bp_opcode_bytes.size(), verify_bytes_read); if (error.Fail()) return error.ToError(); // 确保验证读取的字节数与预期一致 if (verify_bytes_read != verify_bp_opcode_bytes.size()) { return llvm::createStringError( llvm::inconvertibleErrorCode(), "验证断点时读取内存失败:尝试读取 {0} 字节, 但实际只读取了 {1} 字节", verify_bp_opcode_bytes.size(), verify_bytes_read); } // 验证读取到的指令是否与写入的陷阱指令一致 if (llvm::ArrayRef(verify_bp_opcode_bytes.data(), verify_bytes_read) != *expected_trap) { return llvm::createStringError( llvm::inconvertibleErrorCode(), "软件断点写入验证失败 - 在地址 {0:x} 设置断点后, 未能读取到正确的陷阱指令", addr); } // 记录断点设置成功的日志 LLDB_LOG(log, "地址 = {0:x}:设置成功", addr); // 返回包含引用计数(初始为1)、原始指令和陷阱指令的断点对象 return SoftwareBreakpoint{1, saved_opcode_bytes, *expected_trap}; } ``` 这里把源代码的提示字符串也写成的中文, 函数执行逻辑如下: - 获取当前架构的软件断点陷阱指令字节码 - 保存要设置断点地址的指令并替换为陷阱指令 - 验证是否替换成功 - 将原指令字节码返回, 用于触发断点后恢复. #### SetHardwareBreakpoint ```cpp // 设置硬件断点 // 参数: // addr: 要设置断点的内存地址 // size: 断点监控的内存大小 // 返回值:操作状态(成功或失败信息) Status NativeProcessProtocol::SetHardwareBreakpoint(lldb::addr_t addr, size_t size) { // 默认实现假设:为进程设置硬件断点需要为其所有现有线程设置相同的硬件断点 // 新线程创建时也会执行相同的设置操作 Log *log = GetLog(LLDBLog::Process); // 更新线程列表 UpdateThreads(); // 获取硬件调试支持信息(如支持的硬件断点数量等) auto hw_debug_cap = GetHardwareDebugSupportInfo(); // 检查目标是否具备所需的硬件断点能力 // 如果不支持硬件断点, 或已达最大数量限制, 则返回错误 if (hw_debug_cap == std::nullopt || hw_debug_cap->first == 0 || hw_debug_cap->first <= m_hw_breakpoints_map.size()) return Status("目标不具备所需数量的硬件断点支持"); // 存储所有成功设置了该硬件断点的线程指针 // 如果任何线程设置失败, 需要回滚已成功设置的线程, 确保状态一致性 std::vector<NativeThreadProtocol *> breakpoint_established_threads; // 为当前进程的每个线程请求设置硬件断点 std::lock_guard<std::recursive_mutex> guard(m_threads_mutex); // 线程安全锁 for (const auto &thread : m_threads) { assert(thread && "线程列表中不应包含空线程!"); // 为单个线程设置硬件断点 Status thread_error = thread->SetHardwareBreakpoint(addr, size); if (thread_error.Success()) { // 记录成功设置断点的线程, 用于可能的回滚操作 breakpoint_established_threads.push_back(thread.get()); } else { // 如果当前线程设置失败, 回滚所有已成功设置的线程 // 确保所有线程状态一致(要么都设置成功, 要么都不设置) for (auto rollback_thread_sp : breakpoint_established_threads) { Status remove_error = rollback_thread_sp->RemoveHardwareBreakpoint(addr); if (remove_error.Fail()) LLDB_LOG(log, "进程 {0} 的线程 {1} 移除硬件断点失败: {2}", GetID(), rollback_thread_sp->GetID(), remove_error); } // 返回导致失败的错误信息 return thread_error; } } // 将新设置的硬件断点注册到进程的硬件断点映射表中 m_hw_breakpoints_map[addr] = {addr, size}; // 返回成功状态 return Status(); } ``` `thread->SetHardwareBreakpoint` 对应了 `NativeThreadLinux` 中的 `SetHardwareBreakpoint`: ```cpp // 为Linux平台的线程设置硬件断点 // 参数: // addr: 要设置硬件断点的内存地址 // size: 断点监控的内存大小 // 返回值:操作状态(成功或失败信息) Status NativeThreadLinux::SetHardwareBreakpoint(lldb::addr_t addr, size_t size) { // 如果线程处于启动状态, 直接返回成功(无需设置) if (m_state == eStateLaunching) return Status(); // 先尝试移除该地址已有的硬件断点(防止重复设置) Status error = RemoveHardwareBreakpoint(addr); // 如果移除操作失败, 返回错误信息 if (error.Fail()) return error; // 通过寄存器上下文设置硬件断点, 获取断点索引 // 硬件断点通常通过调试寄存器实现, 索引用于标识不同的断点 uint32_t bp_index = m_reg_context_up->SetHardwareBreakpoint(addr, size); // 检查是否成功获取有效断点索引 if (bp_index == LLDB_INVALID_INDEX32) return Status("设置硬件断点失败."); // 将地址与断点索引关联存储, 便于后续管理(如移除断点时使用) m_hw_break_index_map.insert({addr, bp_index}); // 返回成功状态 return Status(); } ``` 跟入 `m_reg_context_up->SetHardwareBreakpoint`, 我们还是看 arm64 的实现, 在 `lldb/source/Plugins/Process/Utility/NativeRegisterContextDBReg_arm64.cpp` 文件中: ```cpp // AArch64架构下基于调试寄存器(DBReg)的寄存器上下文类, 实现硬件断点设置 // 参数: // addr: 要设置硬件断点的内存地址 // size: 断点监控的内存大小(AArch64硬件断点有固定要求) // 返回值:成功则返回有效硬件断点索引(对应调试寄存器编号), 失败则返回LLDB_INVALID_INDEX32 uint32_t NativeRegisterContextDBReg_arm64::SetHardwareBreakpoint(lldb::addr_t addr, size_t size) { // 获取断点相关的日志对象 Log *log = GetLog(LLDBLog::Breakpoints); // 记录待设置断点的地址和大小(十六进制格式) LLDB_LOG(log, "addr: {0:x}, size: {1:x}", addr, size); // 第一步:读取当前硬件断点和观察点的调试信息(如已用断点数量、寄存器状态) llvm::Error error = ReadHardwareDebugInfo(); // 若读取调试信息失败, 记录错误日志并返回无效索引 if (error) { LLDB_LOG_ERROR( log, std::move(error), "无法设置断点:读取调试寄存器信息失败:{0}"); return LLDB_INVALID_INDEX32; } // 初始化变量:control_value用于配置断点控制寄存器, bp_index存储找到的空闲断点索引 uint32_t control_value = 0, bp_index = 0; // 第二步:校验断点大小合法性(AArch64硬件断点仅支持4字节大小) if (size != 4) return LLDB_INVALID_INDEX32; // AArch64硬件断点的无效大小 // 第三步:校验断点地址对齐合法性(AArch64硬件断点要求地址4字节对齐) if (addr & 0x03) // 按位与0x03, 结果非0表示地址未对齐 return LLDB_INVALID_INDEX32; // 无效地址, 需4字节对齐 // 第四步:配置断点控制寄存器的值 // g_enable_bit:断点使能位(置1表示启用该断点) // g_pac_bits:指针认证码相关配置位(AArch64安全特性) // GetSizeBits(size):根据断点大小获取对应的配置位(此处size=4, 对应固定值) control_value = g_enable_bit | g_pac_bits | GetSizeBits(size); // 第五步:遍历所有支持的硬件断点, 寻找空闲索引 bp_index = LLDB_INVALID_INDEX32; // 初始化为无效索引 for (uint32_t i = 0; i < m_max_hbp_supported; i++) { // m_max_hbp_supported是AArch64支持的最大硬件断点数量 if (!BreakpointIsEnabled(i)) { // 检查第i个断点是否未启用(空闲) bp_index = i; // 标记当前空闲的断点索引(会更新为最后一个空闲索引) } else if (m_hbp_regs[i].address == addr) { // 若第i个已启用断点的地址与当前地址相同 return LLDB_INVALID_INDEX32; // 不支持重复地址的硬件断点, 返回无效索引 } } // 若遍历后未找到任何空闲断点索引, 返回无效索引 if (bp_index == LLDB_INVALID_INDEX32) return LLDB_INVALID_INDEX32; // 第六步:更新本地缓存中的断点信息(m_hbp_regs是硬件断点寄存器信息的本地缓存) m_hbp_regs[bp_index].real_addr = addr; // 存储断点的实际地址 m_hbp_regs[bp_index].address = addr; // 存储断点的配置地址(与real_addr一致, 部分场景可能需区分) m_hbp_regs[bp_index].control = control_value; // 存储断点的控制配置值 // 第七步:通过ptrace系统调用, 将本地缓存的断点信息写入硬件调试寄存器 // eDREGTypeBREAK表示操作类型为"硬件断点寄存器" error = WriteHardwareDebugRegs(eDREGTypeBREAK); // 若写入调试寄存器失败, 回滚本地缓存(清空该断点地址、禁用断点) if (error) { m_hbp_regs[bp_index].address = 0; // 清空断点地址 m_hbp_regs[bp_index].control &= ~1; // 清除使能位(禁用断点) // 记录写入失败的错误日志 LLDB_LOG_ERROR( log, std::move(error), "无法设置断点:写入调试寄存器失败:{0}"); return LLDB_INVALID_INDEX32; } // 所有步骤成功, 返回有效的硬件断点索引 return bp_index; } bool NativeRegisterContextDBReg_arm64::BreakpointIsEnabled(uint32_t bp_index) { if ((m_hbp_regs[bp_index].control & g_enable_bit) != 0) return true; else return false; } ``` 这个 `WriteHardwareDebugRegs` 实现在 `lldb/source/Plugins/Process/Linux/NativeRegisterContextLinux_arm64.cpp` 中: ```cpp // Linux AArch64架构寄存器上下文类, 实现硬件调试寄存器(断点/观察点)的写入 // 参数:hwbType - 调试寄存器类型, 区分是硬件断点(eDREGTypeBREAK)还是硬件观察点(eDREGTypeWATCH) // 返回值:llvm::Error类型, 无错误则表示写入成功, 有错误则包含具体失败信息 llvm::Error NativeRegisterContextLinux_arm64::WriteHardwareDebugRegs(DREGType hwbType) { // 定义ptrace系统调用所需的结构体:iovec用于传递数据缓冲区, user_hwdebug_state存储调试寄存器状态 struct iovec ioVec; struct user_hwdebug_state dreg_state; int regset; // 标识要操作的调试寄存器集合类型(断点/观察点) // 将调试寄存器状态结构体初始化为0, 避免未初始化的垃圾数据影响 memset(&dreg_state, 0, sizeof(dreg_state)); // 关联数据缓冲区:ioVec的基地址指向调试寄存器状态结构体 ioVec.iov_base = &dreg_state; // 根据调试寄存器类型(断点/观察点), 配置寄存器集合类型和数据长度, 并填充调试信息 switch (hwbType) { // 处理硬件观察点(监控内存读写) case eDREGTypeWATCH: // NT_ARM_HW_WATCH是Linux定义的AArch64硬件观察点寄存器集合标识 regset = NT_ARM_HW_WATCH; // 计算ioVec的数据长度:包含调试信息头(dbg_info)、填充字段(pad)和所有观察点寄存器数据 ioVec.iov_len = sizeof(dreg_state.dbg_info) + sizeof(dreg_state.pad) + (sizeof(dreg_state.dbg_regs[0]) * m_max_hwp_supported); // 将本地缓存的观察点信息(地址、控制配置)填充到调试寄存器状态结构体 for (uint32_t i = 0; i < m_max_hwp_supported; i++) { dreg_state.dbg_regs[i].addr = m_hwp_regs[i].address; // 观察点监控地址 dreg_state.dbg_regs[i].ctrl = m_hwp_regs[i].control; // 观察点控制配置(如读写监控类型) } break; // 处理硬件断点(监控指令执行) case eDREGTypeBREAK: // NT_ARM_HW_BREAK是Linux定义的AArch64硬件断点寄存器集合标识 regset = NT_ARM_HW_BREAK; // 计算ioVec的数据长度:包含调试信息头(dbg_info)、填充字段(pad)和所有断点寄存器数据 ioVec.iov_len = sizeof(dreg_state.dbg_info) + sizeof(dreg_state.pad) + (sizeof(dreg_state.dbg_regs[0]) * m_max_hbp_supported); // 将本地缓存的断点信息(地址、控制配置)填充到调试寄存器状态结构体 for (uint32_t i = 0; i < m_max_hbp_supported; i++) { dreg_state.dbg_regs[i].addr = m_hbp_regs[i].address; // 断点地址 dreg_state.dbg_regs[i].ctrl = m_hbp_regs[i].control; // 断点控制配置(如使能位、地址对齐) } break; } // 调用Linux ptrace系统调用的PTRACE_SETREGSET命令, 将调试寄存器状态写入内核 // 参数说明:PTRACE_SETREGSET(设置寄存器集合)、线程ID、寄存器集合类型(regset)、数据缓冲区(ioVec)、数据长度 // ToError()将ptrace调用结果转换为llvm::Error类型返回 return NativeProcessLinux::PtraceWrapper(PTRACE_SETREGSET, m_thread.GetID(), ®set, &ioVec, ioVec.iov_len) .ToError(); } ``` 实际上是在通过 ptrace 设置 debug 寄存器, 并且做好记录和检测. 最后让我们再回到 `Resume` 方法, 跟入 `ResumeThread`: #### ResumeThread ```cpp // 恢复Linux系统上的原生进程线程 // 参数: // thread - 要恢复的Linux原生线程 // state - 线程恢复后的状态(运行或单步执行) // signo - 恢复线程时发送的信号编号 Status NativeProcessLinux::ResumeThread(NativeThreadLinux &thread, lldb::StateType state, int signo) { // 获取日志对象, 用于记录线程相关的日志信息 Log *const log = GetLog(POSIXLog::Thread); // 记录当前要恢复的线程ID LLDB_LOG(log, "tid: {0}", thread.GetID()); // 在恢复线程之前, 先检查是否有未完成的停止通知 // 这种情况可能有问题, 因为理论上我们应该等所有线程都停止后 // 再发送未完成的通知, 但这里我们却在发送前恢复了线程 if (m_pending_notification_tid != LLDB_INVALID_THREAD_ID) { LLDB_LOG(log, "即将根据显式请求恢复线程 {0}, 但存在未完成的停止通知(线程 {1}), " "该通知正在等待此线程停止.这是有效的事件序列吗?", thread.GetID(), m_pending_notification_tid); } // 请求恢复线程.这应该是同步操作, 完成后系统应反映线程正在运行 switch (state) { case eStateRunning: { // 恢复线程并使其进入运行状态 const auto resume_result = thread.Resume(signo); // 如果恢复成功, 更新进程状态为运行中 if (resume_result.Success()) SetState(eStateRunning, true); return resume_result; } case eStateStepping: { // 使线程单步执行 const auto step_result = thread.SingleStep(signo); // 如果单步执行成功, 更新进程状态为运行中 if (step_result.Success()) SetState(eStateRunning, true); return step_result; } default: // 处理未识别的线程状态 LLDB_LOG(log, "未处理的状态 {0}.", state); llvm_unreachable("恢复操作遇到未处理的状态"); } } ``` 先来看看 `thread.Resume`, 对应了 `NativeThreadLinux` 类的 `Resume` 方法: ```cpp // 恢复线程执行 // 参数: // signo - 恢复线程时要发送的信号编号, LLDB_INVALID_SIGNAL_NUMBER表示不发送信号 Status NativeThreadLinux::Resume(uint32_t signo) { // 线程将进入运行状态 const StateType new_state = StateType::eStateRunning; // 记录线程状态变更(如果日志功能启用) MaybeLogStateChange(new_state); // 更新线程状态为运行中 m_state = new_state; // 清除线程的停止信息:重置停止原因和停止描述 m_stop_info.reason = StopReason::eStopReasonNone; m_stop_description.clear(); // 如果当前线程没有设置任何监视点(watchpoint), 说明这可能是一个新线程 // 需要为其设置进程中已存在的所有监视点 if (m_watchpoint_index_map.empty()) { // 获取当前线程所属的进程 NativeProcessLinux &process = GetProcess(); // 获取进程中所有已设置的监视点 const auto &watchpoint_map = process.GetWatchpointMap(); // 先清除当前线程的所有硬件监视点 m_reg_context_up->ClearAllHardwareWatchpoints(); // 为当前线程设置所有进程级别的监视点 for (const auto &pair : watchpoint_map) { const auto &wp = pair.second; SetWatchpoint(wp.m_addr, wp.m_size, wp.m_watch_flags, wp.m_hardware); } } // 如果当前线程没有设置任何硬件断点, 为其设置进程中所有活跃的硬件断点 if (m_hw_break_index_map.empty()) { // 获取当前线程所属的进程 NativeProcessLinux &process = GetProcess(); // 获取进程中所有已设置的硬件断点 const auto &hw_breakpoint_map = process.GetHardwareBreakpointMap(); // 先清除当前线程的所有硬件断点 m_reg_context_up->ClearAllHardwareBreakpoints(); // 为当前线程设置所有进程级别的硬件断点 for (const auto &pair : hw_breakpoint_map) { const auto &bp = pair.second; SetHardwareBreakpoint(bp.m_addr, bp.m_size); } } // 准备传递给ptrace的参数数据 intptr_t data = 0; // 如果指定了有效的信号编号, 则将其作为数据传递 if (signo != LLDB_INVALID_SIGNAL_NUMBER) data = signo; // 通过ptrace系统调用继续线程执行, PTRACE_CONT命令用于恢复进程运行 // 并可选择性地发送指定信号 return NativeProcessLinux::PtraceWrapper(PTRACE_CONT, GetID(), nullptr, reinterpret_cast<void *>(data)); } ``` 刷新所有与硬件有关的断点, 然后通过 `PtraceWrapper` 发送 `ptrace` 的 `PTRACE_CONT` 命令, 恢复进程运行. (`SetWatchpoint` 与 `SetHardwareBreakpoint` 类似, 不再赘述) 再来看看 `thread.SingleStep`, 对应了 `NativeThreadLinux` 类的 `SingleStep` 方法: ```cpp // 使线程单步执行 // 参数: // signo - 单步执行时要发送的信号编号, LLDB_INVALID_SIGNAL_NUMBER表示不发送信号 Status NativeThreadLinux::SingleStep(uint32_t signo) { // 线程将进入单步执行状态 const StateType new_state = StateType::eStateStepping; // 记录线程状态变更(如果日志功能启用) MaybeLogStateChange(new_state); // 更新线程状态为单步执行中 m_state = new_state; // 清除线程的停止原因 m_stop_info.reason = StopReason::eStopReasonNone; // 如果还没有单步执行的 workaround(兼容处理), 则获取一个 // 若已有 workaround, 不重新设置, 避免现有实例的析构函数在新实例获取CPU掩码后运行 // 防止线程最终使用错误的掩码 if(!m_step_workaround) { m_step_workaround = SingleStepWorkaround::Get(m_tid); } // 准备传递给ptrace的参数数据 intptr_t data = 0; // 如果指定了有效的信号编号, 则将其作为数据传递 if (signo != LLDB_INVALID_SIGNAL_NUMBER) data = signo; // 如果硬件支持单步执行, 则使用PTRACE_SINGLESTEP命令 // 否则使用PTRACE_CONT命令(此时下一条指令的断点已在NativeProcessLinux::Resume中设置) return NativeProcessLinux::PtraceWrapper( GetProcess().SupportHardwareSingleStepping() ? PTRACE_SINGLESTEP : PTRACE_CONT, m_tid, nullptr, reinterpret_cast<void *>(data)); } ``` 硬件单步对应 `PTRACE_SINGLESTEP` 命令, 软件单步对应 `PTRACE_CONT` 命令(下一条已在 `NativeProcessLinux::Resume` 中设被替换为断点指令). 至此我们都旅程结束. ### Linux 系统中进程状态控制相关的信号 `/usr/include/x86_64-linux-gnu/bits/signum-arch.h` ```cpp /* 针对大多数 Linux 系统的信号编号常量进行调整与补充 */ #define SIGSTKFLT 16 /* 栈错误信号(已过时).早期用于报告栈相关错误, 现代 Linux 系统中已很少使用 */ #define SIGPWR 30 /* 电源故障预警信号.通常由系统硬件或电源管理模块发送, 通知进程电源即将中断(如电池耗尽) */ /* POSIX 标准定义的历史信号(早期标准化的基础信号) */ #define SIGBUS 7 /* 总线错误信号.当进程访问无效内存地址(如未对齐内存访问、物理内存不存在)时触发 */ #define SIGSYS 31 /* 非法系统调用信号.进程调用了不存在或无效的系统调用(如传入错误的系统调用号)时触发 */ /* 较新的 POSIX 标准信号(符合 1003.1-2008、1003.1-2013 标准, 扩展了基础信号功能) */ #define SIGURG 23 /* 紧急数据通知信号.当 socket 上有紧急数据(如带外数据)到达时, 发送给相关进程 */ #define SIGSTOP 19 /* 强制停止信号(不可阻塞/忽略).无论进程如何设置, 都会强制暂停执行, 调试场景常用 */ #define SIGTSTP 20 /* 终端停止信号.通常由用户按下 Ctrl+Z 触发, 进程可忽略或捕获(如自定义暂停逻辑) */ #define SIGCONT 18 /* 继续执行信号.用于恢复被 SIGSTOP 或 SIGTSTP 暂停的进程, 若进程未暂停则信号被忽略 */ #define SIGCHLD 17 /* 子进程状态变化信号.当子进程终止、暂停或恢复时, 内核向父进程发送此信号 */ #define SIGTTIN 21 /* 后台进程读终端信号.后台进程尝试从控制终端读取数据时, 会被发送此信号并暂停 */ #define SIGTTOU 22 /* 后台进程写终端信号.后台进程尝试向控制终端写入数据时, 会被发送此信号并暂停 */ #define SIGPOLL 29 /* 可轮询事件触发信号(System V 风格).当指定的 I/O 事件(如文件描述符可读/可写)发生时触发 */ #define SIGXFSZ 25 /* 文件大小超限信号.进程尝试写入文件导致文件大小超过系统限制(如 ulimit 设置)时触发 */ #define SIGXCPU 24 /* CPU 时间超限信号.进程使用的 CPU 时间超过系统限制(如 ulimit 设置)时触发 */ #define SIGVTALRM 26 /* 虚拟定时器超时信号.基于进程占用的虚拟时间(用户态时间)的定时器超时后触发 */ #define SIGPROF 27 /* 性能分析定时器超时信号.基于进程占用的总时间(用户态+内核态时间)的定时器超时后触发 */ #define SIGUSR1 10 /* 用户自定义信号 1.POSIX 预留的用户可自由使用的信号, 无默认行为, 需进程自行注册处理函数 */ #define SIGUSR2 12 /* 用户自定义信号 2.与 SIGUSR1 类似, 供用户扩展使用(如进程间自定义通信) */ /* 所有现代 POSIX 系统(包括 BSD 和 Linux)均支持的非标准信号(虽非早期 POSIX 标准, 但已成为通用信号) */ #define SIGWINCH 28 /* 窗口大小变化信号(源自 4.3 BSD 和 Sun 系统).当终端窗口大小(行数/列数)改变时, 发送给前台进程 */ /* 为兼容性保留的旧名称(历史遗留名称, 与现有信号名等价, 避免旧代码报错) */ #define SIGIO SIGPOLL /* I/O 就绪信号(源自 4.2 BSD).早期 BSD 系统中表示“I/O 操作可执行”, 现与 SIGPOLL 等价 */ #define SIGIOT SIGABRT /* IOT 指令信号(源自 PDP-11 架构).早期表示触发 IOT 硬件指令, 现与 SIGABRT(进程异常终止)等价 */ #define SIGCLD SIGCHLD /* 旧 System V 系统中的子进程信号名.现与 SIGCHLD 等价, 用于兼容 System V 风格的旧代码 */ #define __SIGRTMIN 32 /* 实时信号的起始编号.POSIX 实时信号从 32 开始, 支持按优先级排队, 适用于需要低延迟的场景 */ #define __SIGRTMAX 64 /* 实时信号的结束编号.Linux 系统中实时信号范围为 32-64, 共 33 个(不同系统可能略有差异) */ ``` ### ptrace 一些头文件 一定程度上反映了 ptrace 的功能: - `/usr/include/x86_64-linux-gnu/sys/ptrace.h` ```cpp /* * 定义 `ptrace` 系统调用中 "REQUEST" 参数的类型 * 注:每个枚举值对应 `ptrace` 的一项调试功能, 是调试器与内核交互的核心指令 */ enum __ptrace_request { /* * 表示发起该请求的进程(通常是子进程)愿意被跟踪 * 作用:子进程主动开放调试权限, 此后它收到的所有信号都会被父进程拦截, * 父进程可通过其他 `ptrace` 请求控制该子进程(如查看内存、单步执行) */ PTRACE_TRACEME = 0, #define PT_TRACE_ME PTRACE_TRACEME // 兼容旧版的宏定义, 功能与 PTRACE_TRACEME 完全一致 /* * 读取被跟踪进程“代码段”(text space, 存储程序指令的内存区域)中指定地址(ADDR)的数据 * 注:按“字”(word, 通常为 4/8 字节, 取决于架构)对齐读取 */ PTRACE_PEEKTEXT = 1, #define PT_READ_I PTRACE_PEEKTEXT // 宏别名, "I" 代表 Instruction(指令), 强调用于读取代码 /* * 读取被跟踪进程“数据段”(data space, 存储全局/静态变量的内存区域)中指定地址(ADDR)的数据 * 注:同样按“字”对齐读取, 用于查看进程的变量值 */ PTRACE_PEEKDATA = 2, #define PT_READ_D PTRACE_PEEKDATA // 宏别名, "D" 代表 Data(数据), 强调用于读取数据 /* * 读取被跟踪进程“用户态寄存器区”(user area, 存储寄存器状态的内存区域)中指定偏移(ADDR)的数据 * 作用:可读取通用寄存器(如 eax、pc)、状态寄存器等硬件寄存器的值 */ PTRACE_PEEKUSER = 3, #define PT_READ_U PTRACE_PEEKUSER // 宏别名, "U" 代表 User(用户态), 强调用于读取用户态寄存器 /* * 将数据(DATA)写入被跟踪进程“代码段”(text space)的指定地址(ADDR) * 典型场景:设置软件断点(如将目标指令替换为 int 3 断点指令) */ PTRACE_POKETEXT = 4, #define PT_WRITE_I PTRACE_POKETEXT // 宏别名, 对应 PT_READ_I, 用于修改代码段 /* * 将数据(DATA)写入被跟踪进程“数据段”(data space)的指定地址(ADDR) * 典型场景:修改进程的全局变量、堆内存值, 用于调试时调整进程状态 */ PTRACE_POKEDATA = 5, #define PT_WRITE_D PTRACE_POKEDATA // 宏别名, 对应 PT_READ_D, 用于修改数据段 /* * 将数据(DATA)写入被跟踪进程“用户态寄存器区”(user area)的指定偏移(ADDR) * 典型场景:修改寄存器值(如调整程序计数器 pc 跳转到指定位置执行) */ PTRACE_POKEUSER = 6, #define PT_WRITE_U PTRACE_POKEUSER // 宏别名, 对应 PT_READ_U, 用于修改用户态寄存器 /* * 让处于暂停状态的被跟踪进程继续执行 * 注:可附带一个信号参数(如传递 0 表示忽略之前的暂停信号) */ PTRACE_CONT = 7, #define PT_CONTINUE PTRACE_CONT // 宏别名, 语义更直观, 强调“继续执行” /* * 强制终止被跟踪进程 * 原理:向被跟踪进程发送 SIGKILL 信号, 进程无法捕获或忽略该信号, 会立即退出 */ PTRACE_KILL = 8, #define PT_KILL PTRACE_KILL // 宏别名, 语义更直观, 强调“终止进程” /* * 让被跟踪进程“单步执行” * 作用:进程仅执行一条指令后就暂停, 并触发 SIGTRAP 信号通知调试器 * 典型场景:逐行调试代码, 观察每一步指令的执行效果 */ PTRACE_SINGLESTEP = 9, #define PT_STEP PTRACE_SINGLESTEP // 宏别名, 语义更直观, 强调“单步执行” /* * 一次性读取被跟踪进程的“所有通用寄存器”状态 * 注:通用寄存器包括 eax/ebx(x86)、r0-r15(ARM)、程序计数器(pc)、栈指针(sp)等 */ PTRACE_GETREGS = 12, #define PT_GETREGS PTRACE_GETREGS // 宏别名, 保持命名一致性 /* * 一次性设置被跟踪进程的“所有通用寄存器”状态 * 典型场景:调试时修改程序执行流程(如修改 pc 跳转到指定函数) */ PTRACE_SETREGS = 13, #define PT_SETREGS PTRACE_SETREGS // 宏别名, 保持命名一致性 /* * 读取被跟踪进程的“所有浮点寄存器”状态 * 注:浮点寄存器用于处理浮点数运算(如 x87 浮点单元、SSE 寄存器) */ PTRACE_GETFPREGS = 14, #define PT_GETFPREGS PTRACE_GETFPREGS // 宏别名, "FP" 代表 Floating Point(浮点) /* * 设置被跟踪进程的“所有浮点寄存器”状态 * 典型场景:调试浮点数运算相关的bug, 修改浮点寄存器值验证逻辑 */ PTRACE_SETFPREGS = 15, #define PT_SETFPREGS PTRACE_SETFPREGS // 宏别名, 保持命名一致性 /* * 附加到一个“已运行”的进程 * 原理:附加后会向目标进程发送 SIGSTOP 信号, 强制其暂停, 调试器可后续控制 * 典型场景:调试已启动的进程(如服务程序), 而非从启动时就跟踪 */ PTRACE_ATTACH = 16, #define PT_ATTACH PTRACE_ATTACH // 宏别名, 语义更直观, 强调“附加进程” /* * 与通过 PTRACE_ATTACH 附加的进程解除调试关系 * 作用:解除后进程恢复独立运行, 不再受调试器控制 */ PTRACE_DETACH = 17, #define PT_DETACH PTRACE_DETACH // 宏别名, 语义更直观, 强调“解除附加” /* * 读取被跟踪进程的“所有扩展浮点寄存器”状态 * 注:扩展浮点寄存器是对普通浮点寄存器的扩展(如 AVX 寄存器、ARM NEON 寄存器) */ PTRACE_GETFPXREGS = 18, #define PT_GETFPXREGS PTRACE_GETFPXREGS // 宏别名, "FPX" 代表 Extended Floating Point(扩展浮点) /* * 设置被跟踪进程的“所有扩展浮点寄存器”状态 * 典型场景:调试使用高级浮点指令(如 AVX)的程序 */ PTRACE_SETFPXREGS = 19, #define PT_SETFPXREGS PTRACE_SETFPXREGS // 宏别名, 保持命名一致性 /* * 让被跟踪进程继续执行, 并在“进入系统调用”或“从系统调用返回”时暂停 * 典型场景:跟踪进程的系统调用流程(如查看 open、read 等系统调用的参数和返回值) */ PTRACE_SYSCALL = 24, #define PT_SYSCALL PTRACE_SYSCALL // 宏别名, 语义更直观, 强调“跟踪系统调用” /* * 读取 GDT(全局描述符表)中的 TLS(线程本地存储)条目 * 作用:获取线程专属的内存区域信息, 用于调试线程本地变量相关问题 */ PTRACE_GET_THREAD_AREA = 25, #define PT_GET_THREAD_AREA PTRACE_GET_THREAD_AREA // 宏别名, 保持命名一致性 /* * 修改 GDT(全局描述符表)中的 TLS(线程本地存储)条目 * 作用:调整线程的本地存储配置, 较少直接使用, 主要用于底层调试 */ PTRACE_SET_THREAD_AREA = 26, #define PT_SET_THREAD_AREA PTRACE_SET_THREAD_AREA // 宏别名, 保持命名一致性 #ifdef __x86_64__ /* * x86_64 架构专用, 用于访问 TLS(线程本地存储)数据 * 注:仅在 64 位 x86 系统中生效, 功能与 GET/SET_THREAD_AREA 类似, 但接口更适配 x86_64 */ PTRACE_ARCH_PRCTL = 30, # define PT_ARCH_PRCTL PTRACE_ARCH_PRCTL // 宏别名, "ARCH" 代表架构相关 #endif /* * 让被跟踪进程继续执行, 并在“下一次进入系统调用”时暂停, 且**不执行该系统调用** * 作用:模拟系统调用触发, 但跳过实际执行, 用于调试系统调用参数验证逻辑 */ PTRACE_SYSEMU = 31, #define PT_SYSEMU PTRACE_SYSEMU // 宏别名, "SYSEMU" 代表 System Call Emulation(系统调用模拟) /* * 让被跟踪进程“单步执行”, 且若下一步是系统调用, 则**不执行该系统调用** * 作用:结合单步调试与系统调用模拟, 用于精细验证指令级逻辑与系统调用的交互 */ PTRACE_SYSEMU_SINGLESTEP = 32, #define PT_SYSEMU_SINGLESTEP PTRACE_SYSEMU_SINGLESTEP // 宏别名, 保持命名一致性 /* * 让被跟踪进程执行, 直到遇到“下一个被执行的分支指令”(如 jmp、call、ret)时暂停 * 典型场景:按“代码块”调试(跳过无分支的连续指令), 提高调试效率 */ PTRACE_SINGLEBLOCK = 33, #define PT_STEPBLOCK PTRACE_SINGLEBLOCK // 宏别名, "STEPBLOCK" 代表按块单步 /* * 设置 `ptrace` 的过滤选项 * 作用:配置调试器接收的事件类型(如是否跟踪子进程、是否在 exec 时通知、是否忽略某些信号) * 注:需配合特定的选项参数(如 PTRACE_O_TRACECLONE、PTRACE_O_TRACEEXEC)使用 */ PTRACE_SETOPTIONS = 0x4200, #define PT_SETOPTIONS PTRACE_SETOPTIONS // 宏别名, 保持命名一致性 /* * 获取上一次 `ptrace` 事件的附加信息(事件消息) * 典型场景:如跟踪子进程创建时, 获取新子进程的 PID;跟踪 exec 时, 获取新程序路径 */ PTRACE_GETEVENTMSG = 0x4201, #define PT_GETEVENTMSG PTRACE_GETEVENTMSG // 宏别名, 保持命名一致性 /* * 获取被跟踪进程收到的信号详情(siginfo_t 结构体) * 作用:获取信号的来源(如哪个进程发送)、触发原因(如硬件错误、软件触发)等细节 */ PTRACE_GETSIGINFO = 0x4202, #define PT_GETSIGINFO PTRACE_GETSIGINFO // 宏别名, 保持命名一致性 /* * 向被跟踪进程发送自定义的信号详情(siginfo_t 结构体) * 作用:模拟特定来源或原因的信号, 用于调试进程的信号处理逻辑 */ PTRACE_SETSIGINFO = 0x4203, #define PT_SETSIGINFO PTRACE_SETSIGINFO // 宏别名, 保持命名一致性 /* * 读取被跟踪进程的寄存器内容(通用接口) * 优势:支持不同架构和寄存器组(如通用寄存器、浮点寄存器、向量寄存器), 比 GETREGS 更灵活 * 注:需指定寄存器组类型(如 NT_PRSTATUS 代表通用寄存器) */ PTRACE_GETREGSET = 0x4204, #define PTRACE_GETREGSET PTRACE_GETREGSET // 宏别名, 保持命名一致性 /* * 设置被跟踪进程的寄存器内容(通用接口) * 优势:与 GETREGSET 对应, 支持多架构和多寄存器组, 适配复杂调试场景 */ PTRACE_SETREGSET = 0x4205, #define PTRACE_SETREGSET PTRACE_SETREGSET // 宏别名, 保持命名一致性 /* * 轻量级附加进程, 功能类似 PTRACE_ATTACH, 但有两个关键区别: * 1. 不强制让被跟踪进程暂停(需后续调用 PTRACE_INTERRUPT 手动暂停) * 2. 不影响被跟踪进程的信号状态和组暂停状态 * 典型场景:需要低侵入性附加进程, 避免打断进程正常执行流程 */ PTRACE_SEIZE = 0x4206, #define PTRACE_SEIZE PTRACE_SEIZE // 宏别名, 保持命名一致性 /* * 让通过 PTRACE_SEIZE 附加的被跟踪进程暂停 * 原理:向目标进程发送 SIGSTOP 信号, 强制其进入暂停状态, 以便后续调试操作 */ PTRACE_INTERRUPT = 0x4207, #define PTRACE_INTERRUPT PTRACE_INTERRUPT // 宏别名, 语义更直观, 强调“中断进程” /* * 等待被跟踪进程组的下一个事件(如子进程创建、信号触发) * 作用:配合 PTRACE_SEIZE 使用, 让调试器进入等待状态, 直到被跟踪进程组发生指定事件 */ PTRACE_LISTEN = 0x4208, #define PTRACE_LISTEN PTRACE_LISTEN // 宏别名, 语义更直观, 强调“监听事件” /* * 读取被跟踪进程的信号队列中的 siginfo_t 结构体, 但**不将信号从队列中移除** * 作用:批量查看进程待处理的信号, 且不影响信号的后续处理(如避免信号被调试器消费后进程无法接收) */ PTRACE_PEEKSIGINFO = 0x4209, #define PTRACE_PEEKSIGINFO PTRACE_PEEKSIGINFO // 宏别名, 保持命名一致性 /* * 获取被跟踪进程当前的“信号阻塞掩码” * 作用:查看哪些信号被进程暂时阻塞(阻塞期间信号不会被处理, 会暂存到信号队列) */ PTRACE_GETSIGMASK = 0x420a, #define PTRACE_GETSIGMASK PTRACE_GETSIGMASK // 宏别名, 保持命名一致性 /* * 修改被跟踪进程的“信号阻塞掩码” * 作用:手动阻塞或解除阻塞某些信号, 用于调试进程的信号处理优先级 */ PTRACE_SETSIGMASK = 0x420b, #define PTRACE_SETSIGMASK PTRACE_SETSIGMASK // 宏别名, 保持命名一致性 /* * 获取被跟踪进程的 seccomp BPF 过滤规则 * 注:seccomp 是进程安全机制, 通过 BPF 规则限制进程可调用的系统调用, 此请求用于查看这些规则 */ PTRACE_SECCOMP_GET_FILTER = 0x420c, #define PTRACE_SECCOMP_GET_FILTER PTRACE_SECCOMP_GET_FILTER // 宏别名, 保持命名一致性 /* * 获取被跟踪进程的 seccomp BPF 过滤规则的元数据 * 作用:查看 BPF 过滤规则的附加信息(如规则数量、加载时间、关联的进程ID) */ PTRACE_SECCOMP_GET_METADATA = 0x420d, #define PTRACE_SECCOMP_GET_METADATA PTRACE_SECCOMP_GET_METADATA // 宏别名, 保持命名一致性 /* * 获取被跟踪进程当前系统调用的详细信息 * 作用:比 PTRACE_SYSCALL 更精细, 可获取系统调用号、参数地址、调用阶段(进入/返回)等 */ PTRACE_GET_SYSCALL_INFO = 0x420e, #define PTRACE_GET_SYSCALL_INFO PTRACE_GET_SYSCALL_INFO // 宏别名, 保持命名一致性 /* * 获取被跟踪进程的 rseq(restartable sequences, 可重启序列)配置信息 * 注:rseq 是内核机制, 用于优化多线程程序的同步操作, 此请求用于调试 rseq 相关逻辑 */ PTRACE_GET_RSEQ_CONFIGURATION = 0x420f #define PTRACE_GET_RSEQ_CONFIGURATION PTRACE_GET_RSEQ_CONFIGURATION // 宏别名, 保持命名一致性 }; ``` - `/usr/include/x86_64-linux-gnu/bits/ptrace-shared.h` ```cpp /* * 通过 PTRACE_SETOPTIONS 请求设置的调试选项枚举 * 作用:控制调试器需要跟踪的事件类型(如进程创建、程序替换、退出等) * 注:多个选项可通过“按位或”组合使用(如 PTRACE_O_TRACEFORK | PTRACE_O_TRACEEXEC) */ enum __ptrace_setoptions { PTRACE_O_TRACESYSGOOD = 0x00000001, // 标记系统调用相关信号:让 SIGTRAP 信号的最低位设为 1, // 方便调试器区分“系统调用触发的 SIGTRAP”与“其他原因的 SIGTRAP” PTRACE_O_TRACEFORK = 0x00000002, // 跟踪进程 fork 事件:当被跟踪进程调用 fork 创建子进程时, // 会触发 PTRACE_EVENT_FORK 事件并通知调试器 PTRACE_O_TRACEVFORK = 0x00000004, // 跟踪进程 vfork 事件:类似 fork, 但针对 vfork(父进程会阻塞到子进程 exec/exit), // 触发 PTRACE_EVENT_VFORK 事件 PTRACE_O_TRACECLONE = 0x00000008, // 跟踪进程 clone 事件:当被跟踪进程调用 clone 创建线程/子进程时, // 触发 PTRACE_EVENT_CLONE 事件 PTRACE_O_TRACEEXEC = 0x00000010, // 跟踪进程 exec 事件:当被跟踪进程调用 execve 加载新程序时, // 触发 PTRACE_EVENT_EXEC 事件(调试器可借此重新设置断点) PTRACE_O_TRACEVFORKDONE = 0x00000020, // 跟踪 vfork 完成事件:当 vfork 创建的子进程执行 exec/exit 后, // 触发 PTRACE_EVENT_VFORK_DONE 事件, 通知父进程可继续执行 PTRACE_O_TRACEEXIT = 0x00000040, // 跟踪进程退出事件:当被跟踪进程调用 exit 或异常退出时, // 触发 PTRACE_EVENT_EXIT 事件, 调试器可获取退出状态 PTRACE_O_TRACESECCOMP = 0x00000080, // 跟踪 seccomp 事件:当被跟踪进程触发 seccomp BPF 过滤规则时, // 触发 PTRACE_EVENT_SECCOMP 事件(用于安全调试) PTRACE_O_EXITKILL = 0x00100000, // 调试器退出时终止被跟踪进程:若调试器意外退出(如崩溃), // 内核会自动终止所有被该调试器跟踪的进程, 避免残留僵尸进程 PTRACE_O_SUSPEND_SECCOMP = 0x00200000, // 暂停被跟踪进程的 seccomp 规则:让被跟踪进程暂时跳过 seccomp 检查, // 仅在调试期间生效, 调试结束后恢复 PTRACE_O_MASK = 0x003000ff // 所有调试选项的掩码:用于验证用户传入的选项是否合法(按位与操作过滤无效位) }; /* * ptrace 调试事件编码枚举 * 作用:与 __ptrace_setoptions 对应, 标识调试器接收到的具体事件类型 * 场景:调试器通过 waitpid 获取事件后, 需通过该枚举判断是“fork事件”“exec事件”还是“退出事件”等 */ enum __ptrace_eventcodes { /* 以下事件码与上述 __ptrace_setoptions 选项一一对应 */ PTRACE_EVENT_FORK = 1, // 对应 PTRACE_O_TRACEFORK:被跟踪进程执行 fork 完成 PTRACE_EVENT_VFORK = 2, // 对应 PTRACE_O_TRACEVFORK:被跟踪进程执行 vfork 完成 PTRACE_EVENT_CLONE = 3, // 对应 PTRACE_O_TRACECLONE:被跟踪进程执行 clone 完成 PTRACE_EVENT_EXEC = 4, // 对应 PTRACE_O_TRACEEXEC:被跟踪进程执行 execve 加载新程序完成 PTRACE_EVENT_VFORK_DONE = 5, // 对应 PTRACE_O_TRACEVFORKDONE:vfork 子进程执行 exec/exit 完成 PTRACE_EVENT_EXIT = 6, // 对应 PTRACE_O_TRACEEXIT:被跟踪进程退出 PTRACE_EVENT_SECCOMP = 7, // 对应 PTRACE_O_TRACESECCOMP:被跟踪进程触发 seccomp 规则 /* 以下事件码不依赖选项, 由内核主动触发 */ PTRACE_EVENT_STOP = 128 // 通用暂停事件:如被跟踪进程收到 SIGSTOP 信号、调试器调用 PTRACE_INTERRUPT 等 }; /* * PTRACE_GET_SYSCALL_INFO 请求的“系统调用阶段”枚举 * 作用:标识被跟踪进程当前处于系统调用的哪个阶段, 用于调试器获取对应阶段的信息 */ enum __ptrace_get_syscall_info_op { PTRACE_SYSCALL_INFO_NONE = 0, // 无有效阶段:未处于系统调用流程中 PTRACE_SYSCALL_INFO_ENTRY = 1, // 系统调用入口阶段:被跟踪进程刚进入系统调用(尚未执行内核逻辑) PTRACE_SYSCALL_INFO_EXIT = 2, // 系统调用退出阶段:被跟踪进程已完成系统调用(即将返回用户态) PTRACE_SYSCALL_INFO_SECCOMP = 3 // seccomp 触发的系统调用阶段:系统调用因触发 seccomp 规则被拦截 }; /* * PTRACE_PEEKSIGINFO 请求的参数结构体 * 作用:传递“读取被跟踪进程信号队列”的配置信息, 支持批量读取且不删除信号 */ struct __ptrace_peeksiginfo_args { __uint64_t off; /* 读取起始偏移:从信号队列的第 off 个信号开始读取(0 表示从第一个开始) */ __uint32_t flags; /* 读取标志:目前仅支持 PTRACE_PEEKSIGINFO_SHARED(读取进程级共享信号队列) */ __int32_t nr; /* 读取数量:期望读取的 siginfo 结构体个数(实际读取数可能少于该值, 取决于队列剩余信号) */ }; /* * PTRACE_PEEKSIGINFO 请求的标志枚举 * 作用:控制信号读取的范围(进程级/线程级) */ enum __ptrace_peeksiginfo_flags { /* 从进程级共享信号队列读取信号(而非当前线程的私有队列) */ PTRACE_PEEKSIGINFO_SHARED = (1 << 0) }; /* * PTRACE_SECCOMP_GET_METADATA 请求的参数与结果结构体 * 作用:传递“读取 seccomp BPF 过滤规则元数据”的输入参数, 并存储输出结果 */ struct __ptrace_seccomp_metadata { __uint64_t filter_off; /* 输入:过滤规则的偏移索引(0 表示第一个规则, 1 表示第二个, 依此类推) */ __uint64_t flags; /* 输出:该过滤规则的标志(如是否为默认规则、是否强制生效等) */ }; /* * PTRACE_GET_SYSCALL_INFO 请求的结果结构体 * 作用:存储被跟踪进程当前系统调用的详细信息, 根据系统调用阶段(entry/exit/seccomp)提供不同字段 */ struct __ptrace_syscall_info { __uint8_t op; /* 系统调用阶段:取值为 __ptrace_get_syscall_info_op 枚举(entry/exit/seccomp) */ __uint32_t arch __attribute__ ((__aligned__ (4))); /* 架构标识:如 AUDIT_ARCH_X86_64(x86_64 架构), * __aligned__(4) 确保字段按 4 字节对齐, 兼容不同编译器 */ __uint64_t instruction_pointer; /* 指令指针:当前执行到的内存地址(即系统调用指令的地址) */ __uint64_t stack_pointer; /* 栈指针:当前栈顶的内存地址(用于定位系统调用参数在栈中的位置) */ union // 联合体:根据 op(系统调用阶段)选择对应的字段, 节省内存 { /* 系统调用入口阶段(PTRACE_SYSCALL_INFO_ENTRY)的信息:包含调用号和参数 */ struct { __uint64_t nr; // 系统调用号:如 5(x86_64 的 open 调用)、63(read 调用) __uint64_t args[6]; // 系统调用参数:最多支持 6 个参数(符合 Linux 系统调用规范), // args[0] 是第一个参数, args[1] 是第二个, 依此类推 } entry; /* 系统调用退出阶段(PTRACE_SYSCALL_INFO_EXIT)的信息:包含返回值和错误标记 */ struct { __int64_t rval; // 系统调用返回值:成功时为非负值(如 open 返回文件描述符), // 失败时为负的错误码(如 -ENOENT 表示文件不存在) __uint8_t is_error; // 错误标记:1 表示系统调用失败, 0 表示成功 } exit; /* seccomp 触发阶段(PTRACE_SYSCALL_INFO_SECCOMP)的信息:包含调用号、参数和 seccomp 返回数据 */ struct { __uint64_t nr; // 系统调用号(同 entry 阶段) __uint64_t args[6]; // 系统调用参数(同 entry 阶段) __uint32_t ret_data; // seccomp 返回数据:seccomp 规则中 SECCOMP_RET_TRACE 携带的自定义数据 } seccomp; }; }; /* * PTRACE_GET_RSEQ_CONFIGURATION 请求的结果结构体 * 作用:存储被跟踪进程的 rseq(restartable sequences, 可重启序列)配置信息 * 注:rseq 是内核优化多线程同步的机制, 该结构体用于调试 rseq 相关逻辑 */ struct __ptrace_rseq_configuration { __uint64_t rseq_abi_pointer; // rseq ABI 结构体的内存地址:指向进程中 rseq 配置的核心数据结构 __uint32_t rseq_abi_size; // rseq ABI 结构体的大小:用于验证结构体版本兼容性 __uint32_t signature; // 签名:用于校验 rseq 配置的合法性(防止篡改) __uint32_t flags; // 配置标志:如是否启用 rseq、是否支持嵌套 rseq 等 __uint32_t pad; // 填充字段:确保结构体总大小按 8 字节对齐(兼容 64 位系统) }; /* * ptrace 系统调用的函数声明 * 功能:实现进程跟踪的核心接口, 根据 request 参数执行不同调试操作 * 参数说明: * - __request:调试请求类型(取值为 __ptrace_request 枚举, 如 PTRACE_ATTACH、PTRACE_CONT 等) * - ...:可变参数, 根据 request 不同而变化, 通用格式为(pid_t PID, void *ADDR, int DATA, void *ADDR2): * - PID:被跟踪进程的 ID(除 PTRACE_TRACEME 外, 所有请求都需指定) * - ADDR:内存地址/偏移(如 PEEKTEXT 读取的地址、POKETEXT 写入的地址) * - DATA:整数数据(如 POKETEXT 写入的值、CONT 继续执行时附带的信号) * - ADDR2:扩展地址参数(部分请求使用, 如 GETREGSET 中的寄存器组描述符地址) * 返回值:成功时返回非负值(具体值因请求而异), 失败时返回 -1 并设置 errno */ extern long int ptrace (enum __ptrace_request __request, ...) __THROW; ```
回复或点赞可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2025-11-5 12:32 被nothing233编辑 ,原因:
#基础理论
#系统相关
#源码框架
收藏
・
10
点赞
・
57
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_51951meggadf3df
非常支持你的观点!
2026-9-11 15:58
whohh
这个讨论对我很有帮助,谢谢!
2026-8-24 20:33
mb_lthgjpwj
为你点赞!
2026-8-20 17:37
mb_cebxltxx
为你点赞!
2026-8-16 08:41
Wika
感谢你的积极参与,期待更多精彩内容!
2026-8-8 16:15
mb_wsoacceo
为你点赞!
2026-8-8 01:25
afishlong
谢谢你的细致分析,受益匪浅!
2026-7-14 03:27
mb_hsscwjpz
谢谢你的细致分析,受益匪浅!
2026-7-9 20:50
erniu222
为你点赞!
2026-3-13 21:49
mb_szqldxer
期待更多优质内容的分享,论坛有你更精彩!
2026-3-13 14:30
homea_qwq
非常支持你的观点!
2026-1-3 19:29
Dy1an
为你点赞!
2025-12-24 17:22
npc0vo
你的分享对大家帮助很大,非常感谢!
2025-11-26 19:27
zapata_ok
非常支持你的观点!
2025-11-25 13:15
hyz111
感谢你分享这么好的资源!
2025-11-19 17:15
逆向小玖
期待更多优质内容的分享,论坛有你更精彩!
2025-11-19 13:05
哈哥
感谢你的积极参与,期待更多精彩内容!
2025-10-29 13:38
mb_wdhsjycl
感谢你分享这么好的资源!
2025-10-23 17:50
陈某人
感谢你的贡献,论坛因你而更加精彩!
2025-10-22 11:43
sk97
非常支持你的观点!
2025-10-21 21:13
xianyuuuan
非常支持你的观点!
2025-10-20 10:41
pexillove
期待更多优质内容的分享,论坛有你更精彩!
2025-10-20 00:19
Cherzsh
感谢你的贡献,论坛因你而更加精彩!
2025-10-18 10:45
F2zZ
谢谢你的细致分析,受益匪浅!
2025-10-17 15:25
git_21210wzzzh2025
感谢你的贡献,论坛因你而更加精彩!
2025-10-17 12:37
git_47499test-look
这个讨论对我很有帮助,谢谢!
2025-10-16 18:26
逆天而行
谢谢你的细致分析,受益匪浅!
2025-10-16 13:04
mb_hfvycjls
谢谢你的细致分析,受益匪浅!
2025-10-16 11:23
mb_kgfqphsx
为你点赞!
2025-10-16 09:12
孤独的街
感谢你分享这么好的资源!
2025-10-15 22:14
MsScotch
非常支持你的观点!
2025-10-15 10:46
cydian
你的分享对大家帮助很大,非常感谢!
2025-10-15 10:33
qqizai
感谢你的积极参与,期待更多精彩内容!
2025-10-15 09:24
快乐的小跳蛙
感谢你分享这么好的资源!
2025-10-14 22:29
我的小拇指啊
非常支持你的观点!
2025-10-14 17:06
灬哈密瓜
感谢你的积极参与,期待更多精彩内容!
2025-10-14 17:00
爱吃菠菜
期待更多优质内容的分享,论坛有你更精彩!
2025-10-14 09:29
龙飞雪
期待更多优质内容的分享,论坛有你更精彩!
2025-10-13 19:27
mb_rqgxdcwg
这个讨论对我很有帮助,谢谢!
2025-10-13 19:03
yixinBC
感谢你的积极参与,期待更多精彩内容!
2025-10-13 15:40
岁月。
感谢你的积极参与,期待更多精彩内容!
2025-10-13 15:02
nonovo
你的帖子非常有用,感谢分享!
2025-10-13 14:49
tuosen
谢谢你的细致分析,受益匪浅!
2025-10-13 14:45
醉染
非常支持你的观点!
2025-10-13 11:58
jjjo
感谢你分享这么好的资源!
2025-10-13 11:33
令狐双
你的帖子非常有用,感谢分享!
2025-10-13 10:31
mb_oowzftna
你的帖子非常有用,感谢分享!
2025-10-13 10:23
juice4fun
感谢你的积极参与,期待更多精彩内容!
2025-10-13 10:02
便胜晴天
这个讨论对我很有帮助,谢谢!
2025-10-13 09:13
顽劣
你的帖子非常有用,感谢分享!
2025-10-12 18:24
fanfall
你的分享对大家帮助很大,非常感谢!
2025-10-12 17:16
ONewTach
谢谢你的细致分析,受益匪浅!
2025-10-12 16:00
huangyalei
你的分享对大家帮助很大,非常感谢!
2025-10-12 15:31
mb_bppcorlj
你的帖子非常有用,感谢分享!
2025-10-12 15:27
doduhuang
为你点赞!
2025-10-12 15:07
Hcheng
谢谢你的细致分析,受益匪浅!
2025-10-12 13:14
sinker_
你的分享对大家帮助很大,非常感谢!
2025-10-12 10:48
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
15
)
huangjw
雪 币:
6679
活跃值:
(11862)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
510
粉丝
2
关注
私信
huangjw
2
楼
牛啊
2025-10-12 10:04
0
mb_ldbucrik
雪 币:
6
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
691
粉丝
7
关注
私信
mb_ldbucrik
3
楼
太牛啦
2025-10-12 11:41
0
mb_nkjfyhkg
雪 币:
298
活跃值:
(170)
能力值:
( LV2,RANK:10 )
在线值:
发帖
4
回帖
78
粉丝
9
关注
私信
mb_nkjfyhkg
4
楼
mark
2025-10-13 02:55
0
iBa0
雪 币:
1560
活跃值:
(5833)
能力值:
( LV4,RANK:40 )
在线值:
发帖
6
回帖
482
粉丝
37
关注
私信
iBa0
5
楼
1
2025-10-13 14:23
0
Ms135
雪 币:
9
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
89
粉丝
0
关注
私信
Ms135
6
楼
666
2025-10-13 17:19
0
Imxz
雪 币:
112
活跃值:
(9405)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
750
粉丝
9
关注
私信
Imxz
7
楼
tql
2025-10-13 17:20
0
mb_qimctavn
雪 币:
5219
活跃值:
(6615)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
385
粉丝
1
关注
私信
mb_qimctavn
8
楼
6
2025-10-17 10:57
0
mb_lizfxfno
雪 币:
518
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
24
粉丝
0
关注
私信
mb_lizfxfno
9
楼
666
2025-10-17 15:59
0
mb_lghhumnu
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
22
粉丝
0
关注
私信
mb_lghhumnu
10
楼
谢谢分享
2025-10-23 10:47
0
mb_cizqyhuh
雪 币:
2
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
81
粉丝
1
关注
私信
mb_cizqyhuh
11
楼
666
2025-10-28 11:44
0
我是小奥
雪 币:
114
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
49
粉丝
0
关注
私信
我是小奥
12
楼
谢谢分享
2025-11-19 12:29
0
fengvmu
雪 币:
5874
活跃值:
(3705)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
69
粉丝
0
关注
私信
fengvmu
13
楼
感谢分享
2025-12-21 12:35
0
GEKEZYX
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
154
粉丝
0
关注
私信
GEKEZYX
14
楼
666
2025-12-22 09:51
0
xiaoc996
雪 币:
938
活跃值:
(2255)
能力值:
( LV3,RANK:30 )
在线值:
发帖
4
回帖
47
粉丝
35
关注
私信
xiaoc996
15
楼
tql
2025-12-22 11:49
0
wx_Huber Barrientos
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
206
粉丝
1
关注
私信
wx_Huber Barrientos
16
楼
666
2026-4-25 07:36
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
nothing233
1
17
发帖
18
回帖
90
RANK
关注
私信
他的文章
[原创]LLVM: ADCEPass
2454
[原创]llvm: InstSimplifyPass
3780
[原创]一个 ELF 文件的运行
34835
[原创]capstone学习: 反汇编器如何实现的
15458
[原创]chromium 的启动
3257
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部