Fix 使用 GDB 调试 Clang 编译的程序时标准库类型始终显示 incomplete type

现象描述

系统是 Linux mint 18,其实就是 ubuntu 16.04 LTS,所用的 clang 版本是 apt 默认的 3.8;gdb 使用系统自带。

使用 clang 编译出来的二进制,哪怕开启了 -g 生成调试信息,在 gdb 下 std::string 始终显示为 incomplete type;而 primitive 类型的值可以正确显示

并且通过 apt 安装 lldb,使用 lldb 调试能够看到变量的值。

因此基本可以确定问题发生在 gdb 和 clang 这一对上

解决方案

来自路边社的资料:clang 的 linux 版本输出调试信息时,默认去掉了标准库相关的组件信息 (??? MMP)

在编译时追加 -fno-limit-debug-info 标志,就可以保证输出完整的调试信息

安利时刻

为什么使用 clang 编译的原因很简单,ubuntu 16.04 上的 apt 默认的 gcc/g++ 5.4 有 bug,编译我的项目代码一定会出错…对比直接从 ppa test 安装 gcc 6.x/7.x 而言,安装 clang 无疑最省事儿…

另外我发现这个问题的解决方案得益于 vscode-lldb-debugger 这个扩展。

正是这个扩展在输出里通过 warning 告诉我需要用相关的编译开关输出完整调试信息,这个问题才的可以解决。(从侧面说明程序员还是应该关注一下 warning XD)

最后安利一个 gdb gui 前端:Nemiver

不过现在看起来 linux 上最好的 gdb/lldb 前端应该是 vscode -.-

Monthly Read Posts in Aug 2017

How to Feel Progress

一篇讲述如何自我管理的 post

核心内容大概就是:

  • Making progress in meaningful work.
  • Break a large project into several small pieces.

Lightning Talks: Anatomy of A Smart Pointer

适合新手扫盲

话说作者其实在 cppcon 2015 也做了一个类似的 talk,但是没有找到视频,ppt 倒是和14年这个 tech talk 的一致


C++ devirtualization in clang

编译器有时候会根据上下文,自动 inline 某些虚函数调用。

这个 talk 讲的就是 clang 在这方面的一些细节。

不做编译期,对细节并不是很感兴趣。

另外这个小哥应该是个俄罗斯人,口语发音实在是太卧槽了


C++ WAT

和上面那个 tech talk 一个小哥,口语听起来想死…

这个 tech talk 专门介绍了一些 C++ 里的坑(UB),作为茶余饭后的点心稍微过一下就差不多了。

工作里要是有人这么写代码,直接拖出去打死。


TCP TIME-WAIT 笔记 - 概览
TCP Flow Control

第一篇本身就是某些 post 的笔记,有时间可以读读原文

第二篇非常细致的解释了 flow control

实话说,理论性的教科书看了很多时候还是云里雾里的,最后还是需要实际工程的洗礼;要不怎么说人的认知是螺旋式上升的呢


Make a class thread safe C++

这篇文章看起来很长,其实就说了一点:两个原子的函数的复合函数自身并不保证原子……

….好吧,如果比较闲的话可以看看。


Thinking in Parallel

核心观点:thinking in parallel is to think about your data as being made up of small fixed-size subset of data that can be dealt with independently.


Shared Libraries: Understanding Dynamic Loading

这篇 post 比较有意思,算是墙裂推荐。

非常工程的叙述如何在 Linux 上使用 shared library,非常好的扫盲科普文。


How to write documentation that’s actually useful

前篇论证为什么要写文档;举例使用的 truck factor theory 挺好玩的…

后面解释应该怎么写文档

讲道理,我觉得如何写文档还真是一个和具体事情强相关的活,除非你和 MSDN 一样可以找到专门撰写的外包团队


A Brief History of the UUID

前半部分是 UUID 的考古,作者文笔还不错,可以当作阅读材料…

后半部分提作者根据自己业务实现的一个 KSUID,优点是吸收 timestamp,可以 k-sortable ordered

先 mark 一下,没准哪天需要

Controlled Type Injection in C++

之前实现 kbase 的 ENSURE 宏支持如下用法

1
2
3
4
5
// Throw a std::runtime_error when condition is violated.
ENSURE(RAISE, cond)(var).Require();
// Throw a MyException when condition is violated.
ENSURE(RAISE, cond)(var).Require<MyException>();

这几天在重新审视的时候觉得将自定义异常类型依托 Require() 注入是一个有问题的设计:强行耦合了 RAISE 这个行为的具体策略和行为无关的 Require() 函数,在设计语义上给人感觉莫名其妙。

更加糟糕的是,因为 ENSURE 宏依托的 Guarantor 类不是一个 class template,自定义异常类的信息没办法直接保存在类中,导致 Require<MyException>() 的重载实现非常丑陋…

于是我想,如果能将异常类型注入通过一个单独的函数完成,并且保存在类成员中,那上面提到的两个问题都可以得到解决

1
2
3
// Call ThrowIn to configure exception type that will be raised.
// And there is only one Require().
ENSURE(RAISE, cond)(var).ThrowIn<MyException>.Require();

并且由于 ThrowIn() 函数具有 configuration 的语义,哪怕实际操作只是 performed-in-debug-only 的 CHECK,也不会有任何违和感

1
2
// We configured exception type but we don't raise an exception at all.
ENSURE(CHECK, cond)(var).ThrowIn<MyException>.Require();

由于个人极度不希望将 Guarantor 变成一个 class template,因此不能使用常规方法保存这个注入的类型信息,换言之,我们大概需要一种能够将类的类型和他包含的某个成员的类型信息的关联拆开。

然后我就想到了 std::shared_ptr

std::shared_ptr 的神奇之处在于,他的 deleter 不是自己类型的一部分,而且可以在运行期替换,which literally is what we need!

于是我就照猫画虎,写了一个 exception-pump,做类似的事情(不过我只是研究了一下实现逻辑,才没有傻到真的去看 std::shared_ptr 的源代码呢 XD):

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
class ExceptionPump {
public:
virtual ~ExceptionPump() = default;
virtual void Throw(const std::string& what) const = 0;
};
template<typename E>
class ExceptionPumpImpl : public ExceptionPump {
public:
void Throw(const std::string& what) const override
{
throw E(what);
}
};
class Guarantor {
public:
// ...
template<typename E>
Guarantor& ThrowIn()
{
static_assert(std::is_base_of<std::exception, E>::value, "E is not a std::exception");
exception_pump_ = std::make_unique<internal::ExceptionPumpImpl<E>>();
return *this;
}
void Guarantor::DoThrow()
{
if (!exception_pump_) {
exception_pump_ = std::make_unique<internal::ExceptionPumpImpl<EnsureFailure>>();
}
exception_pump_->Throw(exception_desc_.str());
}
private:
std::unique_ptr<internal::ExceptionPump> exception_pump_;
}

exception_pmp_ 仅会在需要是才会创建具体对象实例,因此对仅使用 CHECK 行为的语句来说,这块可以认为是 zero-cost

BTW:在我改完这部分代码之后,我把 RAISE 改名成了 THROW,感觉和上下文更加 consistent 一些

GetEnvironmentVariable, API 设计的反面教材

本次 post 的主角 GetEnvironmentVariableW() 用于获取某个环境变量的值,它的文档可以参见这里,看的时候记得仔细研究一下返回值细节。

这个 API 的设计可以说是一个生动的反面教材。

根据 MSDN,它的返回值如下:

  • 如果成功,则返回保存到缓存中的值的字符数,不包括 terminating null character
  • 如果缓冲区不够,那么就返回需要的缓冲区大小,以字符数计数,且包含 terminating null character
  • 如果函数失败,则返回 0 (具体细节由 GetLastError() 给出)

这里不吐槽在返回值非 0 的前提下,存在两种计数的返回值,我们考虑一种很 rare 的情况:环境变量的值是一个 empty string 时,这个 API 应该返回什么

Windows 上是可以通过 SetEnvironmentVariableW() 将某个环境变量的值设置为空字符串的

1
2
3
4
const wchar_t* env_name = L"KCPath";
const wchar_t* str = L"";
BOOL rv = SetEnvironmentVariableW(env_name, str);
assert(rv);

那么此时用 GetEnvironmentVariableW() 获取值时,会返回 0 (因为不包含 terminating null,字符数为 0)。那么问题来了,如果我事先不知道这是一个空字符串,那么我怎么知道这个返回 0 意味着失败还是空字符串?

答案是没法区分,即使你再进一步调用 GetLastError()

为什么?

因为 GetLastError() 的文档说的很清楚:在大部分 API 失败时, calling thread 的 last error code 会被设置;而有部分(少量)API在成功时,也会设置 last error code。

但是很遗憾,通过实验验证,环境变量相关的 API 不属于后者。

这意味着存在一种可能:首先调用了某个环境变量的API,例如 GetEnvironmentVariableW(),但是他失败了,于是 last error code 被设置了;然后紧接着的代码片段利用 GetEnvironmentVariableW() 获取了某个环境变量的值,而这个值是 empty string,于是 API 返回 0;此时你调用了一下 GetLastError(),他返回了一开始调用 API 错误设置的那个错误值,于是这下你就以为后面的调用失败了….

来上一个 PoC 吧

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
int main()
{
const wchar_t* env_name = L"KCPath";
const wchar_t* str = L"";
// Failed, and last error code will be set.
DWORD size_needed = GetEnvironmentVariableW(env_name, nullptr, 0);
assert(size_needed == 0);
assert(GetLastError() == ERROR_ENVVAR_NOT_FOUND);
// set an empty string.
BOOL rv = SetEnvironmentVariableW(env_name, str);
assert(rv);
// Request buffer size from the API.
// Returned size includes null-terminate character.
size_needed = GetEnvironmentVariableW(env_name, nullptr, 0);
assert(size_needed == 1);
wchar_t buf[MAX_PATH] {0};
DWORD size_read = GetEnvironmentVariableW(env_name, buf, MAX_PATH);
// WAT? is last call failed?
assert(size_read == 0);
auto err = GetLastError();
// Boom!
assert(err == ERROR_ENVVAR_NOT_FOUND);
return 0;
}

这个故事告诉我们,一个接口的返回值语义的清晰和不重叠是多么的重要。

最后:这个故事来自于我一个很意外的单元测试用例;我一开始一度以为问题出在 GetEnvironmentVariableW() 不能正确处理空字符串值,以至于这篇 post 最开始的标题是 the werid behavior of GetEnvironmentVariable;但是在我编写 PoC 的阶段我却发现问题怎么也复现不出来…经过快有半小时的跟踪分析后,我才最终确定真正的 root cause;这也告诉我们一个道理,verification 是多么的重要…

Monthly Read Posts in June 2017

Starting a tech startup with C++

又名,Facebook 开源项目宣传广告。

不过实话说,以 Facebook 写 C++ 那部分代码的恣意姿态,如果团队的平均水平够不到中上的槛,弄不好项目还没发布自己就先挂了。

Andrei Alexendrescu:我仿佛听见有人在背后黑我…


Tips for Productive Debugging with GDB
CppCon 2015: Greg Law “ Give me 15 minutes & I’ll change your view of GDB”

GDB Tips…

第二个是 CppCon 2015 的一个 talk,感觉还凑合吧。不过实话说,15分钟我都能撸一发了,何必浪费时间在这个上…上个靠谱的前端不才应该是正确的姿势么…


CppCOn 2015: Visualize Template Instantiation

这个 talk 的团队基于 Eclipse 做了一个 Cevelop,杀手锏功能就是 able to visualize template instantiation

不过比较蛋疼的是演示关键部分的时候,摄像机没有给对位置…

(希望 JetBrains 早点吧这个功能抄过去…)

PS:这个 speaker 神似我的初中语文老师….


A better date and time C++ library

安利了 Howard Hinnant 写的一个 date lib。

而且据说这个 lib 很有机会进入标准库;现在标准库的 chrono 只是针对 timing 设计而没有对 date 的有力支持确实太蛋疼了


理解基于 TCP 的应用层通信协议

大水文一篇,基本没有一点营养。

不客气地说,现在国内开发者刊物上,大部分内容都是这种没有什么营养、容易误导读者,而且还浪费时间的文章。

挂出来批判一下

就是要你懂 TCP
就是要你懂 TCP | 最经典的TCP性能问题
关于TCP 半连接队列和全连接队列

阿里中间件团队官方博客的三篇 TCP 相关分享。

算是有水平的内容文。

第一篇介绍了一下 TCP 的基础内容;第二篇介绍了 delayed ack 和 nagle’s algorithm;第三篇涉及 sync queue 和 accept queue 的处理问题

TCP Peculiarities for Games, part 1
TCP Peculiarities as Applied to Games, Part II

一个游戏服务端开发大牛的两篇关于游戏服务端 TCP 开发相关的文章。

两篇文章内容不光已经涵盖了前面阿里中间件团队的前两篇内容,还谈到了一些容易被忽略的点,例如 Head-of-Line Blocking。

第二篇还专门针对游戏服务端的业务特点,给了一些建议

利用 Job 内核对象实现父进程关闭时自动结束所有子进程

投稿工具的压制功能一直来都有一个问题:如果主进程被强行结束了(例如利用任务管理器),那么创建的 ffmpeg 压制进程仍然会继续运行。

因为 ffmpeg 是以二进制的方式部署的,因此不存在修改它的代码,自己和主进程建立 IPC 监控的方式。

至于采用远线程注入的方式来强行 HACK,我一直对这种无视客观规律的霸道方式都不太感冒,毕竟我们又不是做安全软件。

所以我想到了曾经在 Windows 核心编程中看到的一个方法:使用 Job 内核对象。

核心方法总结起来就是一句话:将 ffmpeg 压制进程加入到一个 Job 对象中,利用 Job 对象的 Kill-On-Close 特性,在 Job 内核对象被释放时,Job 内的所有进程都会被系统结束。

于是我写了一个 demo snippet,解决了几个坑之后验证了我的想法。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <conio.h>
#include <cassert>
#include <iostream>
#include <string>
#include <Windows.h>
bool LaunchAndWait(const std::wstring& cmdline, HANDLE job)
{
wchar_t buf[255] {0};
wcscpy_s(buf, cmdline.c_str());
STARTUPINFO startup {0};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process_information {nullptr};
if (!CreateProcessW(nullptr,
buf,
nullptr,
nullptr,
FALSE,
CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB, // important
nullptr,
nullptr,
&startup,
&process_information)) {
auto err = GetLastError();
std::cerr << "Failed to create process: " << err;
return false;
}
if (!AssignProcessToJobObject(job, process_information.hProcess)) {
std::cerr << "Failed to assign process to job: " << GetLastError();
return false;
}
ResumeThread(process_information.hThread);
CloseHandle(process_information.hProcess);
CloseHandle(process_information.hThread);
return true;
}
int main()
{
BOOL is_in_job;
IsProcessInJob(GetCurrentProcess(), nullptr, &is_in_job);
std::cout << "Is the main process in job: " << is_in_job << std::endl;
std::cout << "Wait for constructing job object!\n";
HANDLE job = CreateJobObjectW(nullptr, nullptr);
if (!job) {
auto err = GetLastError();
std::cerr << "Failed to create job: " << err << std::endl;
return 0;
}
// Enforce limits.
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limits;
memset(&job_limits, 0, sizeof(job_limits));
job_limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &job_limits, sizeof(job_limits))) {
auto err = GetLastError();
std::cerr << "Failed to set job information: " << err << std::endl;
}
std::cout << "Job object created! Launch test subjects...\n";
bool r = LaunchAndWait(LR"(C:\Windows\notepad.exe)", job);
assert(r);
_getch();
return 0;
}

最终的产品代码思路是完全一样的,当然代码不可能像上面这样随意。

这里有两个比较隐蔽的坑(MSDN 上不一定能看到)需要注意一下:

第一个是创建进程时最好带上 CREATE_BREAKAWAY_FROM_JOB,因为 Job 属性存在自动继承性:如果一个进程属于一个 Job,那么他创建的子进程会自动关联到这个 Job。

explorer 和 VS 都是存在于 Job 中的,因此如果在这两个环境下运行程序, IsProcessInJob() 会返回 true

第二个坑特别隐蔽:我一开始的 test subject 是 calc.exe,在 Windows 10 上这是一个 UWP 程序,然而无论我怎么做,calc 进程都活得好好的,一点不像是被约束的样子。。。甚至所有 API 的调用都并没有返回错误。

而当我将 test subject 换成 Win32 程序 notepad.exe 之后,功能就可以正常跑了….

我猜想这大概和 UWP 程序的一些安全特性有关,不过因为本身对安全并没有偏执的热爱,所以在这个点上就没有继续深入了。

Send Specific Cookie in URLFetcher

使用 URLFetcher 作为网络请求的基础组件有一个优势:如果 server 的 response header 指明了 set-cookie,那么 URLFetcher 在发出下一个请求时会自动往 request header 中设置 cookie。

这为使用某些需要在请求时附带 cookie 的 API 提供了便利;但是同时也在某一些场合下导致了一些问题。

如果你需要为某个 API 请求的 request header 中携带自定义的 cookie 数据,例如 cookie: buvid=xxxx,那么,即使你在发送请求前通过 AddExtraRequestHeader() 添加了 cookie 头,你也会惊奇地发现,出去的请求并没有携带你设置的 cookie,而是只有 URLFetcher 自己设置的,服务端提供的 cookie 值。

不要问为什么这个时候要用 cookie 传递这个信息,而不是某个独有的 request header,亦或者 query-string;你永远不知道和你对接的人是一个怎样的智障;毕竟,没有谁会真的去读 RFC。

自定义 cookie 丢失的原因大概如下:

  1. 你首先设置了 cookie,然后发出请求
  2. URLFetcher 在内部实际进行网络请求前,会设置服务端的 cookie 值(因为之前的请求存在 set-cookie
  3. 因为 RFC 规定,一个 request header 中, cookie header line 有且仅有一个,也就是说
1
2
cookie: buvid=xxx
cookie: sig=xxx

是不合法的;虽然可以通过

1
cookie: buvid=xxx; sig=xxx

来传递多个value,但是这要求 URLFetcher 在添加 cookie 时要能够 aware 到这点;很可惜,根据情况来看这点不成立

  1. 于是 URLFetcher 粗暴的替换了你设置的 cookie 值

扫了一遍代码之后,我想到一个 workaround,实践证明确实可以:

发请求前设置 URLFetcher 的 load flag,加上 LOAD_DO_NOT_SEND_COOKIE,阻止 URLFetcher 自己设置 cookie 的行为,完全由自己控制请求的 cookie。

话说用 URLFetcher 被坑了好多次(算上 chromium 其他的 lib,次数可以翻翻),还好每次都能够凭借“优秀工程师的第六感”蒙中问题的 root cause,进而化险为夷。我突然感觉自己理解了什么叫 文章本天成,妙笔偶得之