解决运行 bcdedit 后 BitLocker 提示输入恢复密钥

问题

因为种种原因……需要调试本机内核,用 bcdedit 开启调试模式:

1
bcdedit -debug ON

结果重启后,BitLocker 提示输入恢复密钥!

分析

首先,要强调——调试本机内核,本身就是一个很危险的操作!建议还是用虚拟机调试。

其次,您一定已经备份了 BitLocker 的恢复密钥。只是它不一定在身边,比如说放在家里,人在公司,一来一回需要很长时间,所以得想办法节省时间。

最后,不要被 BitLocker 吓倒!即使,您没有备份恢复密钥,也还有救!在这个界面按 ESC,再点“跳过此驱动器”,后面是可以进入“控制台”的,只需要在“控制台”里撤销操作即可!

解决

尝试关闭调试模式:

1
bcdedit -debug OFF

先别急着重启,因为这么敲——无效!不信您可以重启后再运行 bcdedit,会发现这个 debug 选项还是 Yes。

看来在 WindowsRE 环境下,直接运行 bcdedit,并不能修改 C 盘里的启动选项。您需要加上个 ID,一般为 {default}。

1
bcdedit -debug {default} OFF

先别急着重启,因为这么敲——还是无效!原本没有 debug 这个值,现在多了一个,数据是 No 而已,因为默认值就是 No,看似没有改变“调试模式”,但其实 BCD 数据库是变了的。正确的做法是删除这个 debug 值:

1
bcdedit -deletevalue {default} debug

重启后不再要求输入恢复密钥。这时可以在 BitLocker 的控制面板里先暂停保护,然后再操作 BCD,即可。

为什么应该使用 C++ 20?

实战项目

  1. 鎏光云游戏引擎

  2. 金山云 LiveNet

  3. ClipboardSync

  4. PowerEconomizer

其中,鎏光一开始是用 C++ 17 的,在 MSVC 的 std::format 可用时,第一时间切换到 C++ 20。而 LiveNet 主要运行于 Linux,开发时 gcc 还不支持 std::format,使用 fmt::format 代替,但一直用 cxxstd=20 编译。

ClipboardSync 和 PowerEconomizer 使用了 C++ 20 modules。

现在还在开发的云桌面产品也是使用 C++ 20,但没有用 modules。

理由

根据 jetbrains 的统计,2022 年时,C++ 20 的使用率是 23%,已经超过经典 C++ 的 8%。在游戏开发领域,C++ 20 的使用率为 25%,甚至已经超过 C++ 14 的 24%。

以下列举能够很容易想到的一些好处:

  1. 好用、安全的新类:std::format、std::span、std::jthread、原子(Atomic)智能指针

  2. designated-initializers 安全初始化,防止因为调整结构体而顺序不对

  3. modules 加速编译

  4. 更多标签 [[likely]], [[unlikely]], [[nodiscard(reason)]]

  5. 可以 using enum

  6. 对模板形式的 Lambda 有更好支持

  7. 范围 for 循环支持初始化

  8. 三路比较运算符 <=>

  9. Boost 的 awaitables 协程(BOOST_ASIO_HAS_CO_AWAIT)需要 C++ 20

  10. ranges 库

阵痛

  1. 曾经遇到 clang-format 对 modules 支持不好的问题,后来升级 clang-format 解决。但目前还不建议在大型项目里使用 modules。

  2. 有些隐式转换无法编译,尤其在编译驱动代码时,容易遇到连 WDK 里的头文件都无法编译。这是因为 C++ 20 比 C++ 17 都严格。建议内核态驱动使用 C++ 17;用户态驱动可以 C++ 20。

无银第八哥【2】修改 API 返回结果

需求

想修改 API 的返回结果。举个例子,想把前文提到的 Windows Release 信息改为 “0618”。

winver

分析

从技术角度看,需求就是在 RegQueryValueExW 返回时,改 lpData 指向的内存。

1
2
3
4
5
6
7
8
LSTATUS RegQueryValueExW(
[in] HKEY hKey,
[in, optional] LPCWSTR lpValueName,
LPDWORD lpReserved,
[out, optional] LPDWORD lpType,
[out, optional] LPBYTE lpData,
[in, out, optional] LPDWORD lpcbData
);

lpData 为第 5 个参数,根据《x64 软件约定》,它位于栈。具体来说,断点时,rip 处于 call 指令执行完毕的时间点,这时调用方的返回地址已经被压入栈里,所以此时的栈顶为返回地址。按照内存地址递增方向,返回地址后面就是 6 个参数。

实现

  1. 按照前文方法,断到 DisplayVersion 的读取:
1
2
3
DisplayVersion
KERNELBASE!RegQueryValueExW:
00007ffc`2bd79ff0 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000073`b6cfe618=00007ffc2e6731dc
  1. dq rsp L7 显示返回地址和 6 个参数:
1
2
3
4
5
0:000> dq rsp L7
000000d7`e975e7e8 00007ffc`2e53e6bd 00007ffc`2d600000
000000d7`e975e7f8 00007ffc`2e6731dc 00000268`e4a4a6a0
000000d7`e975e808 00000000`0000003c 00000268`e4a96d10
000000d7`e975e818 000000d7`e975e820

其中,00000268`e4a96d10 对应 lpData,000000d7`e975e820 对于 lpcbData。

  1. 可选地,查看 lpcbData 指向的值,发现它是 0x100:
1
2
0:000> dd 000000d7`e975e820 L1
000000d7`e975e820 00000100
  1. 输入 gu 运行到函数返回,再查看 lpData 的内容:
1
2
3
4
5
0:000> gu
shcore!_SHRegQueryValueW+0x8d:
00007ffc`2e53e6bd 0f1f440000 nop dword ptr [rax+rax]
0:000> du 00000268`e4a96d10
00000268`e4a96d10 "22H2"
  1. 用 eu 指令修改 lpData 的内容:
1
eu 00000268`e4a96d10 "0618"
  1. 输入 g,再继续运行,即可大功告成。

埋前文的坑

  1. 前文“实现”节的第 5 步,bm KERNELBASE!RegQueryValueExW 断不下来?

您可能是在 Windows 10 下实践才遇到这个问题。可以用 x KERNELBASE!RegQueryValueExW* 看看是不是有多个。一般来说,即使有多个,无银第八哥也能都断,用 bl 可以看到多个都被加入。万一多个函数的类型不一样,可能就只加了一个 void 类型的,可以直接用地址指定另外类型的(没有被添加的)。

Windows 11 是这样:

1
2
3
4
5
6
0:000> x KERNELBASE!RegQueryValueExW*
00007ffc`2bd79ff0 KERNELBASE!RegQueryValueExW (void)
00007ffc`2beb2090 KERNELBASE!RegQueryValueExW (void)
0:000> bl
1 e Disable Clear 00007ffc`2bd79ff0 0001 (0001) 0:**** KERNELBASE!RegQueryValueExW "$$<C:\\devel\\windbg\\RegQueryValueExW.txt"
2 e Disable Clear 00007ffc`2beb2090 0001 (0001) 0:**** KERNELBASE!RegQueryValueExW "$$<C:\\devel\\windbg\\RegQueryValueExW.txt"

Windows 10 可能是这样:

1
2
3
0:000> x kernelbase!RegQueryValueExW*
00007ff8`2a400a20 KERNELBASE!RegQueryValueExW (void)
00007ff8`2a303700 KERNELBASE!RegQueryValueExW (RegQueryValueExW)
  1. KERNELBASE!RegQueryValueExW 返回后的指令怪怪的?
1
2
3
0:000> gu
shcore!_SHRegQueryValueW+0x8d:
00007ffc`2e53e6bd 0f1f440000 nop dword ptr [rax+rax]

前一条的 call qword ptr [shcore!_imp_RegQueryValueExW (00007ffc2e5e5c90)]` 指令是 7 字节的,如果为了对齐也不应该补 5 字节呀。

1
2
3
4
5
6
7
8
9
10
0:000> u 00007ffc`2e53e6b6
shcore!_SHRegQueryValueW+0x86:
00007ffc`2e53e6b6 48ff15d3750a00 call qword ptr [shcore!_imp_RegQueryValueExW (00007ffc`2e5e5c90)]
00007ffc`2e53e6bd 0f1f440000 nop dword ptr [rax+rax]
00007ffc`2e53e6c2 8bd8 mov ebx,eax
00007ffc`2e53e6c4 85c0 test eax,eax
00007ffc`2e53e6c6 755a jne shcore!_SHRegQueryValueW+0xf2 (00007ffc`2e53e722)
00007ffc`2e53e6c8 8b55b8 mov edx,dword ptr [rbp-48h]
00007ffc`2e53e6cb 83fa01 cmp edx,1
00007ffc`2e53e6ce 0f85b7000000 jne shcore!_SHRegQueryValueW+0x15b (00007ffc`2e53e78b)

看!后面的指令是从 00007ffc`2e53e6c2 开始的,也没有对齐。所以,如果不是为了对齐,那就可能是为了方便调试时在函数返回时加 int 3 了。如果有新的答案,将在本系列后续文章分享。

无银第八哥【1】条件断点

问题

使用“无银第八哥”给注册表 API 下断点,结果调用极其频繁,如果一个个人工去看,容易逐渐失去耐心。毕竟,挨踢太卷了!

解决

您只需要“条件断点”!但是怎么写“条件”成为拦路虎。

好在,“无银第八哥”自带了一份很简要的学习材料,您可以在帮助菜单打开,或按 F1,或在命令窗口输入 .hh 打开,然后输入“conditional breakpoints”,将进入一篇名为《Conditional breakpoints in WinDbg and other Windows debuggers》的帮助文档。

“条件断点”建议的使用方式是,把条件写到文件里,方便复用。指定一个断点的条件为某个文件内容的语法是:

1
bp function "$$<C:\\commands.txt"

当然,文件的内容才是重点,将在后面的实践例子里讲解。

实践

需求

winver 显示的 Windows 的 Relase 版本信息,比如“22H2”,是从注册表里读的,想断下这个读取。

winver

实现

以下在 Windows 11 x64 下进行。

  1. 下断点前,需要先知道这个 Release 信息保存的注册表位置:
  • 主键:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
  • 值:DisplayVersion
  • 数据:即是需要获取的 Release 信息
  1. 但是 winver.exe 的导入表里并没有任何 Reg API,反而这个程序的核心就是调用 SHELL32!ShellAboutW 而已。

  2. 查看 SHELL32.dll 的导入表,发现有 api-ms-win-core-registry-l1-1-0.dll,但 Reg API 里有两个可以读值,需要做个基本排序,按兼容性推测,使用 RegQueryValueExW 几率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 老 API
LSTATUS RegQueryValueExW(
[in] HKEY hKey,
[in, optional] LPCWSTR lpValueName,
LPDWORD lpReserved,
[out, optional] LPDWORD lpType,
[out, optional] LPBYTE lpData,
[in, out, optional] LPDWORD lpcbData
);

// 新 API
LSTATUS RegGetValueW(
[in] HKEY hkey,
[in, optional] LPCWSTR lpSubKey,
[in, optional] LPCWSTR lpValue,
[in, optional] DWORD dwFlags,
[out, optional] LPDWORD pdwType,
[out, optional] PVOID pvData,
[in, out, optional] LPDWORD pcbData
);
  1. 打开“无银第八哥”,按 Ctrl+E,打开 C:\Windows\System32\winver.exe

  2. 第一个需要尝试的断点是 RegQueryValueExW,输入 bm KERNELBASE!RegQueryValueExW,然后 g,发现可以断下。

  3. 开始考虑“条件断点”,需要针对 lpValueName 判断是否为 “DisplayVersion”。lpValueName 为第二个参数,按照 x64 call,即为 rdx,所以编写 C:\devel\windbg\RegQueryValueExW.txt 代码如下:

1
2
.if (@rdx != 0) { as /mu ${/v:ValueName} @rdx } .else { ad /q ${/v:ValueName} }
.if ($scmp(@"${ValueName}", "DisplayVersion") == 0) { .echo ValueName } .else { .echo ValueName; gc }

然后,输入 bm KERNELBASE!RegQueryValueExW "$$<C:\\devel\\windbg\\RegQueryValueExW.txt",再 g,发现能断下:

1
2
3
DisplayVersion
KERNELBASE!RegQueryValueExW:
00007ffc`2bd79ff0 48895c2410 mov qword ptr [rsp+10h],rbx ss:0000006c`ef69e768=00007ffc2e6731dc
  1. 额外地,可以返回(gu)看看(ub)调用方长啥样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> gu
shcore!_SHRegQueryValueW+0x8d:
00007ffc`2e53e6bd 0f1f440000 nop dword ptr [rax+rax]
0:000> ub
shcore!_SHRegQueryValueW+0x6b:
00007ffc`2e53e69b 44897db0 mov dword ptr [rbp-50h],r15d
00007ffc`2e53e69f 48894c2428 mov qword ptr [rsp+28h],rcx
00007ffc`2e53e6a4 4c8d4db8 lea r9,[rbp-48h]
00007ffc`2e53e6a8 498bcd mov rcx,r13
00007ffc`2e53e6ab 48897c2420 mov qword ptr [rsp+20h],rdi
00007ffc`2e53e6b0 4533c0 xor r8d,r8d
00007ffc`2e53e6b3 488bd0 mov rdx,rax
00007ffc`2e53e6b6 48ff15d3750a00 call qword ptr [shcore!_imp_RegQueryValueExW (00007ffc`2e5e5c90)]

GetEnvironmentStrings 函数的八哥史

故事

故事总由八哥开始!今天稣看到一个 buggy 的 API 声明:

1
2
3
4
5
#ifdef UNICODE
#define GetEnvironmentStrings GetEnvironmentStringsW
#else
#define GetEnvironmentStringsA GetEnvironmentStrings
#endif // !UNICODE

GetEnvironmentStrings

但是经历过 OutputDebugString 逆向的稣十分淡定地推测,这一定是故意的!毕竟,微软为了兼容性,啥都干得出来。

稣的逆向经验:一般 API 都是 A 的版本调用 W,而 OutputDebugString 是例外,OutputDebugStringW 调用 OutputDebugStringA。

搜索知识

找到 Raymond Chen 写的《A brief history of the GetEnvironmentStrings functions》。原来,这个 API 早在 Windows NT 3.1 时就烙下八哥!

The Get­Environment­Strings function has a long and troubled history.

The first bit of confusion is that the day it was introduced in Windows NT 3.1, it was exported funny. The UNICODE version was exported under the name Get­Environment­StringsW, but the ANSI version was exported under the name Get­Environment­Strings without the usual A suffix.

A mistake we have been living with for over two decades.

虽然后来可以解决这个例外,但微软选择保留此例外。

结论

大家可以不必担心相关的可能问题,因为现代的 Windows 会同时导出 GetEnvironmentStrings 和 GetEnvironmentStringsA。

GetEnvironmentStrings functions

Boost【8】shared_library

1. 故事

听说 Boost 有一个跨平台的 shared_library 可以管理动态链接库,试试?

2. 尝试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <format>
#include <iostream>

#include <boost/dll/shared_library.hpp>

int main() {
boost::dll::fs::error_code ec;
boost::dll::shared_library ntdll(
L"ntdll.dll", boost::dll::load_mode::search_system_folders, ec);
if (ec) {
std::cerr << ec.message();
return EXIT_FAILURE;
}

std::cout << std::format("ntdll: {}\n", ntdll.location(ec).string());
bool has = ntdll.has("RtlGetNtSystemRoot");
std::cout << std::format("has RtlGetNtSystemRoot: {}\n", has);

if (has) {
std::wcout << std::format(L"RtlGetNtSystemRoot: {}\n",
ntdll.get<wchar_t*()>("RtlGetNtSystemRoot")());
}

try {
auto RtlGetNtSystemRoot = ntdll.get<wchar_t*()>("RtlGetNtSystemRoot");
if (nullptr != RtlGetNtSystemRoot) {
std::wcout << std::format(L"RtlGetNtSystemRoot: {}\n",
RtlGetNtSystemRoot());
}
} catch (const boost::system::system_error& e) {
std::cerr << e.what();
}
}

3. 思考

上面的例子显然不合格,因为它并不跨平台!动态加载 Windows 特有的 ntdll.dll 应该用 Windows Implementation Library

但作为范例,或者项目已经引入 Boost,却没有引入 wil,也是可以用用,只是它并不极致。比如说,ntdll.dll 其实并不需要 load,它必然被加载,只需要 GetModuleHandle 即可。

所以它其实还不如这个好用:https://github.com/UMU618/umu/blob/main/include/umu/module.hpp

“天道酬勤”的最初含义

转自:https://epaper.gmw.cn/wzb/html/2016-04/16/nw.D110000wzb_20160416_5-02.htm

“天道酬勤”大约是中国人最喜欢说的四个字,也是书法爱好者最爱写的词汇,经常用来励志。

事实上,从它的本义论起,现今我们对于它的理解是大错特错。它既非励志之言,更非一般百姓能用,若非君临天下,您断不可用此四字。

“天道酬勤”并非成语,由《尚书·大诰》:“天閟毖我成功,天亦惟用勤毖我民”之句引申而来。唐人孔颖达疏:“天慎劳民使成功,亦当勤劳民使安宁。”意思是:“上天啊,您咋这么好呢!您如此谨慎地护佑我的事业,确保其成功!您又是如此频繁地关照我的百姓……”这句话原是周天子向上天说的,用来感谢上天对国民无微不至的关怀。

到了近代,人们将其转引为更通俗的“天道酬勤”。字面虽是直白了,但歧义亦由此产生,变成了今天人们所理解的——同志们,要加倍努力啊,到时上天会犒赏大家的(最好发个大红包)。

“天道酬勤”之“勤”非“勤奋”之意,它只是表示次数多,即频繁。“酬勤”二字是倒装句,“勤”是状语,连起来意为“频繁地赏赐”。如此理解,便与我们平时的理解大有不同了。

“天道酬勤”与故宫太和殿上方的“建极绥猷”匾额传达的是一个意思,就是皇帝们要顺应天命,建立人间法则,安抚四方。所以,“天道酬勤”四字,本是君主们才能说的话。

八哥系列:Debian 12 的 xrdp 突然连不上!

八哥就是让你料不到!

故事

用 macOS 的 Microsoft Remote Desktop Beta 连着 Debian 12,切回 macOS 一段时间后,再回 Debian,发现卡死了。

一看 Debian 的机器,是睡眠了,立刻按电源键唤醒。但是 Microsoft Remote Desktop Beta 再也连不上 Debian。一直在连接的界面,也不报错,也不超时。

SSH 到 Debian 一顿治疗后,依然无法连接。最后重启 Microsoft Remote Desktop Beta,居然好了……

折腾记录

八哥一:为什么睡眠了?

因为每次 RDP 进入 KDE 后,都弹出一个密码输入框,上面想着“挂起系统需要身份验证”,于是稣做了如下操作,把它去掉。

sudo vim /etc/polkit-1/rules.d/85-suspend.rules,输入:

1
2
3
4
5
6
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.login1.suspend" &&
subject.isInGroup("tsusers")) {
return polkit.Result.YES;
}
});
1
2
sudo chmod 755 /etc/polkit-1/rules.d
sudo chmod 644 /etc/polkit-1/rules.d/85-suspend.rules

结果——机器就能自动睡眠了。这是不符合预期的……

八哥二:SSH 好好的,唯独 XRDP 坏了?

确实是稣的一大失算!因为期间在 Debian 本地使用了 wayland,而 xrdp 用的是 xorg,怀疑这可能把 xrdp 弄坏,就把目光都集中在 xrdp 上,各种修复,甚至重新安装、配置 xrdp,也无济于事。

另外一个误导因素是:刚用上 京东京造的 SSD,期间摸了摸,觉得烫得不行,担心是 SSD 异常,导致 xrdp 的文件被破坏。事实上,这个破硬盘也确实因为过热自动写保护两次,最后都无法启动系统了。

八哥三:Windows 连 Debian 居然没问题……

这才意识到是 macOS 的 Microsoft Remote Desktop Beta 有问题!连忙重启这个 App,终于真相大白!现在看着这名字末尾的 Beta 陷入沉思。

用 TraceView 取代 DbgView

1. 故事

  • 开发虚拟显示器驱动时,打太多日志到 DbgView,结果导致驱动被底层主动杀死。

  • 有一天,Linux 和 Windows 驱动都精通的钧叔,突然和稣吐槽,WPP 太擸𢶍。

2. 问题

  • OutputDebugStringA 太慢了!

  • WPP 太乱了!

3. 相关知识

  • ETW(Event Tracing for Windows)是 Windows 操作系统中的一种事件跟踪技术,可以用于记录系统和应用程序生成的事件。ETW 的优点就是性能好,并且同时具备内核态和用户态 API。

  • WPP(Windows Software Trace Preprocessor)是一种用于 Windows 软件跟踪的预处理器,可以帮助开发人员在代码中插入跟踪语句,并生成可用于 ETW 的跟踪消息。即,WPP 基于 ETW,只是做了层封装。

  • TraceView is a trace controller and a trace consumer. 类似于用 DbgView 看 OutputDebugStringA 产生的消息。ETW 产生的消息用 TraceView 来看。

4. 解决

慢的,不用就行。乱的,换个用法。

既然已经有 TraceView,那么我们只需要把 OutputDebugStringA 替换为 ETW 的 API 不就完事了吗?说干就干,先写个简单的 Trace Provider 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <Windows.h>

#include <evntprov.h>

#include <cassert>
#include <iostream>
#include <string_view>

class EtwLog {
public:
EtwLog() = default;
~EtwLog() {
if (0 != event_handle_) {
EventUnregister(event_handle_);
}
}

ULONG Initialize(const GUID& guid) noexcept {
ULONG ec = EventRegister(&guid, nullptr, nullptr, &event_handle_);
assert(ERROR_SUCCESS == ec);
return ec;
}

ULONG Log(std::wstring_view message,
std::uint8_t level = 1,
std::uint64_t keyword = 1) noexcept {
assert(0 != event_handle_);
EVENT_DESCRIPTOR event_descriptor;
EVENT_DATA_DESCRIPTOR data_descriptor;
EventDescCreate(&event_descriptor, 0, 0, 0, level, 0, 0, keyword);
EventDataDescCreate(
&data_descriptor, message.data(),
static_cast<ULONG>((message.size() + 1) *
sizeof(std::wstring_view::value_type)));
return EventWrite(event_handle_, &event_descriptor, 1, &data_descriptor);
}

private:
REGHANDLE event_handle_{};
};

int main() {
std::cout << "Hello ETW!\n";

EtwLog etw;
// {aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}
static const GUID guid = {0xaaaaaaaa,
0xaaaa,
0xaaaa,
{0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}};
ULONG ec = etw.Initialize(guid);
if (ERROR_SUCCESS != ec) {
std::cerr << "Failed to Initialize ETW!\n";
return ec;
}
etw.Log(L"Hello ETW!"); // View via TraceView
}

打开 TraceView,做好基本配置:

采用 GUID 方式

没有 TMF,先 Auto,等八哥

然后,运行以上 C++ 代码,回到 TraceView 界面,能看到捕获到信息,但并没有“Hello ETW!”,而是写着“解码错误 1168”。

解码错误

到此,恍然大悟,原来 ETW 太底层,所以才有 WPP 定义一系列规范来使用 ETW,只不过 WPP 太老,不好用了。有没有一种不需要 pdb/man/tmf 的使用 ETW 的方式?

有的。它就是 Windows 10 新增的 TraceLogging

Windows 10 introduces TraceLogging which builds on ETW and provides a simplified way to instrument code for native, .NET and WinRT developers.

TraceLogging is a system for logging events that can be decoded without a manifest. On Windows, TraceLogging is used in user-mode and kernel-mode to generate Event Tracing for Windows (ETW) events. TraceLogging builds on Event Tracing for Windows (ETW) and provides a simplified way to instrument code.

参考微软给的例子就很容易理解并上手:C/C++ TraceLogging Examples

诗盗·如梦令·挨踢垂暮

《诗盗·如梦令·挨踢垂暮》

常记挨踢垂暮,沉重不知归路。

知识宛成咒,误入内卷深处。

要猝,要猝,惊起一滩社畜。

注解

改自宋代李清照的《如梦令·常记溪亭日暮》:

常记溪亭日暮,沉醉不知归路。
兴尽晚回舟,误入藕花深处。
争渡,争渡,惊起一滩鸥鹭。