调内存破坏(memory corruption)最难的地方在于:等你发现数据被写花了,破坏现场早就过去了,你看到的是一堆垃圾值,却不知道是哪个线程、哪行代码写的。一个实用的思路是给可疑缓冲区加一道没有访问权限的守护页(guard page),谁越界写就地触发段错误,调用栈直接停在凶手那一帧。
下面这套 Linux 方法最早是 Liran B.H. 写的;Windows 上把 mprotect 换成 VirtualProtect 即可。
调内存破坏(memory corruption)最难的地方在于:等你发现数据被写花了,破坏现场早就过去了,你看到的是一堆垃圾值,却不知道是哪个线程、哪行代码写的。一个实用的思路是给可疑缓冲区加一道没有访问权限的守护页(guard page),谁越界写就地触发段错误,调用栈直接停在凶手那一帧。
下面这套 Linux 方法最早是 Liran B.H. 写的;Windows 上把 mprotect 换成 VirtualProtect 即可。
线上崩溃通常只回收到一个体积很小的 .dmp(minidump),里面只有崩溃时刻的线程栈、寄存器和模块列表,没有符号。要还原出可读的函数调用栈,需要两样东西:从 .pdb 抽出来的 Breakpad 符号文件,和一个能拿符号去解析 dump 的 walker。这套工具链跨平台、不依赖 Visual Studio,很适合放进 CI 或崩溃收集服务。
.pdb(或 Linux 的 DWARF、macOS 的 dSYM),产出 Breakpad 格式的 .sym 符号文件。.dmp,打印出带函数名/行号的调用栈。托管代码(managed code,C#/VB.NET 等跑在 .NET CLR 上的代码)经常会 P/Invoke 一个 C++ 写的 native dll。这种时候在 native dll 的 C++ 源码里下断点,断点常常是空心的,VS 提示「未为该文档加载任何符号(no symbols have been loaded for this document)」——因为默认的调试器只挂了托管引擎,没有挂 native 引擎。
要让断点命中,需要打开 混合模式调试(mixed-mode debugging),让 VS 同时挂托管和 native 两个调试引擎。步骤如下:
Optick 是一个面向游戏的超轻量 C++ profiler,集成简单、CPU/GPU 能放在同一帧里一起看,适合做日常性能分析和优化。
| 能力 | Windows | Linux | macOS | Xbox | PS4 | UE4 | Switch |
|---|---|---|---|---|---|---|---|
| 插桩(Instrumentation) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | |
| 上下文切换(Switch Context) | ✔ | ✔ | ✔ | ✔ | ✔ | ||
| 采样(Sampling) | ✔ | ✔ | ✔ | ||||
| GPU | ✔ | ✔ | ✔ | ✔ |
几个在大代码库里查 bug 时常用、但容易被忽略的 VS 调试手段:数据断点定位内存被谁改的、诊断工具抓内存泄漏、看 std::mutex 被哪个线程锁住,以及远程调试和临时关函数优化。
数据断点(Data Breakpoint)监视一块内存,一旦它被写就触发中断。它特别适合两类问题:在庞大代码库里查某个值被意外改写的逻辑 bug,以及定位内存破坏——直接告诉你哪一行代码写花了这块内存。
前提是先确定要监视的内存地址,通常是某个变量。下面用一个真实案例走一遍流程:角色会被墙卡住。
MSBuild(Microsoft Build Engine)是 Visual Studio 背后的构建系统。做老引擎移植、跨平台工程时经常要直接和 .vcxproj、工具集打交道,下面是几条实用经验。
工程文件本质是个 XML,所有工程信息都用 XML 元素描述,大致分三类:
ItemGroup 里(杂项文件也可以放进 item)。
ItemDefinitionGroup。比如对 ClCompile item,可以指定 WarningLevel、编译选项等元数据,告诉编译器怎么编它们。PropertyGroup。可以自定义任意属性,用 $(prop_name) 或 %(prop_name) 引用。#include,把任意合法元素引进当前文件。本系列笔记整理自《C++ Concurrency in Action》(Anthony Williams 著),基于 C++11 及之后标准,系统讲解多线程与并发编程:从线程管理、共享数据保护,到内存模型、原子操作,再到无锁数据结构与并发代码设计。适合有一定 C++ 基础、希望深入理解并发原理与工程实践的读者。
std::thread 启动/join/detach、传参、所有权转移、运行期线程数选择future/promise、packaged_task、限时等待在生活中, 最简单和基本的并发是指两个或更多独立的活动(separate activities)同时发生.
计算机领域的并发指的是在单个系统里并行执行多个独立的活动(independent activities),而不是顺序执行或一个接一个地执行.
单核计算机一次只能执行一个任务, 可以通过每秒多次任务切换(task switching)造成“并发的假象”.
每次任务切换时都需要切换上下文(context switch),是需要时间的, 如上图.
每个程序至少有一个线程:执行main()函数的线程是程序的入口点。
其他线程有其各自的入口函数。这些线程与主线程 main() 同时运行。
当线程执行完其入口函数后,线程就会退出。这与main()函数执行完毕后程序结束的情形类似。
为了启动一个线程,你需要创建一个std::thread对象,然后等待这个线程结束。
线程在std::thread对象构造时就开始启动了,无返回值时,启动后自动结束,存在返回值时,参数传递完成后结束。
在C++多线程编程中,确实存在共享数据的问题,特别是当多个线程试图同时修改同一份数据时。
只读操作通常不会导致问题,因为所有线程都可以同时读取相同的数据,而不会对数据产生修改。
当一个或多个线程需要修改共享数据时,就需要特别小心。
不变量(Invariants)是一个非常重要的概念,它可以帮助程序员更好地理解和描述特殊数据结构的状态。