Project RC

一种标准阿西的设计与实现。

2022,确实是一个新的开始

创建于
分类:Misc
标签:年度总结

又是一年没有更新博客了,上一次一年没更新还是准备考研的 2019 年。

今年年初的时候写了一篇 2022,会是一个新的开始吗,很简略,寥寥几笔就把 2021 的一年低谷带过了。去年年底到今年年初,情况逐渐开始好转,随后的一年虽然也有一些起伏,但总体还是好于年初时候对今年的期待。2022 年,确实是一个新的开始,或许是我人生的一个转折点。

OS 助教和毕业设计

今年的上半学期,也就是研二下学期,终于当了一次操作系统课的助教,主要就是参与出 lab 和回答同学们的问题。出 lab 是从实验室的操作系统 ChCore 主线上做裁剪,然后挖出一些空,期间也发现了 ChCore 一些有 bug 和值得重构的地方,又 port 回主线,算是为 ChCore 做的最后贡献了。回答同学们的问题也很有趣,我很喜欢帮到别人的感觉,一些同学也很能抓到要点,许多时候是在相互交流进行思维升级,而不是单方面的回答。

这学期的后半段,另一件最重要的事情就是毕业设计了,是在 ChCore 上尝试一种新的系统服务设计。开题的时候,正逢上海疫情封校封楼,在宿舍里写代码效率很低,当时非常焦虑,担心在暑假前写不完,影响之后的实习。后来焦虑带来了动力,通过几天熬夜快速进入了状态,虽然之后效率又逐渐下降,但还是在暑期实习之前基本写完了毕设的代码。到了 11 月暂停实习之后,则是边摸鱼边写毕业论文,到 12 月初算是把论文基本写完了,虽然直到现在还有需要修改的小地方🤣。

实习、校招和 RisingWave

6 月底的时候终于开始了在 Singularity Data(现在已经改名叫 RisingWave Labs)的实习,参与维护公司开源的 RisingWave 流式数据库内核。其实在实习之前,我根本完全不懂数据库的原理,更不懂流式数据库。刚开始不知道从何下手,后来慢慢做了一些简单的任务,开始进入状态,到最后也算是基本能理解整个数据库的设计思想了(不过感觉以后还是需要补一些比较系统的课程)。公司里面大部分同事都是聪明而有态度的年轻人,和他们一起在开源项目上工作,让我感到极度舒适,这是我一直以来梦寐以求的感觉。

在实习的同时参与了校招,面试的公司不算太多,有一些面到一半已经不想再面于是放弃了,最终真正完成并通过面试的公司只有字节、华为和百度昆仑芯三家。虽然面试的时候感觉发挥都挺好,但由于大环境的压力,这些公司都迟迟没有发 offer,甚至一直到 9 月底都没有收到任何一个意向(后两家其实没有意向阶段),字节则是分别由不同的组捞了好几次。那段时间非常焦虑,但又不想再投简历了,想着就听天由命吧,毕竟转正应该是肯定可以转的。到了 10 月底,面完了的三家公司都陆续给了意向或开奖,焦虑的心情终于迎来解脱。后来,经过和 mentor、和朋友、和自己的充分沟通,最终还是决定在 Singularity Data 转正,想把这种开源、现代、自驱、自由的工作方式继续下去。

OneBot 和 WasmEdge

在这一年断断续续的一些时间点,也尽力推进了之前聊天机器人方向上的开源项目 OneBot。目前 OneBot 12 标准已经基本上稳定,也有了一些对它的实现。由于自己已经很久没有写过聊天机器人,越来越明显地感觉到自己在这个方向上已经不再有激情、动力和洞见了,暂时还不知道该怎么办。不过,看到一些朋友在积极地尝试采用 OneBot 12 标准,还是很开心的。

暑期实习的下班时间还抽空参与了 WasmEdge 社区的一个 LFX Mentorship 项目。项目本身并不是改进 WasmEdge Runtime,而是移植一个数据库的客户端 SDK 到 WasmEdge 上运行,虽然做起来比较简单,但在过程中还是学到了许多 WebAssembly 相关知识,了解了一个 WASM 运行时的基本结构等等。

新冠疫情防控

3 月和 9 月分别经历了两次封校、封宿舍楼,4 月和 11 月分别看到了两次朋友圈“电子游行”,后者也伴随了在各地上演的真实抗议。从 3~5 月上海摇摆后重新“坚持动态清零”,再到“二十条”、“新十条”、全国逐渐取消公共场所核酸要求,最后在 12 月 26 日宣布明年 1 月 8 日开始对新冠病毒实施“乙类乙管”,我想我算是见证了历史,见证了一段浓墨重彩的历史。新冠疫情防控开始于我考完研后的仅仅一个月,结束于我硕士毕业前的一个月,我的整个硕士生涯几乎完全笼罩在疫情和疫情防控造成的不确定性中。现在,这段时期结束,一个新的时期正在开始,我感到悲壮,也感到人类的渺小。

12 月 22 日,我终于第一次感染并发作了新冠,先是发高烧 2~3 天,然后咽痛不断加剧,有三个晚上难以入睡,到今天(27 号),尽管因为不再发烧,精神已经好了很多,但身体状况仍然没有完全恢复。

读书和思考

从年初开始决定记录今年的阅读情况,记录读了什么书、对书的评价,以及一些简单的读后感或是总结。Notion 提供了很好的自定义数据库和视图功能,帮助我方便地管理这些记录,我把今年开始已读的书都公开在了我的 读书 页面。

2022 年已读列表
2022 年的已读列表

最后,2022 年一共读了 16 本书,超出了一开始的预期。一些书补充完善了我的世界观、价值观和人生观,让我对自然、社会、人、世界等一切有了更好的理解。读书的过程,就像曾经某位名人(忘了是谁)所说的,像在和作者对话,这种对话不是单向的听取,而是有来有回且循序渐进的思想交流。这些书中不一定每个观点都让我十分信服,甚至有一些我持反对意见,但它们都让我得到了一些精神上的收获。

除了记录阅读,今年还开始记录了自己对各种事情的思考,希望用文字的形式把这些思考固化下来,以便以后可以找寻自己思维发展的过程。直到年底,已经记录了 24 个思考。我发现把对事物的思考写成文字,可以强迫自己更全面地考虑问题,而不是只对事物的一个方面产生情绪化的反应。

2022 年思考列表
2022 年的思考列表

音乐现场

生日那天,看了人生第一场 livehouse 演出,是房东的猫在苏州的巡演。随后在疫情防控放开后,又分别在上海和杭州看了两场,分别是达闻西乐队和达达乐队的演出。我发现我爱上了 livehouse 这种演出形式,因为在这里,乐队和观众的距离被拉近,乐手和歌手们成了活生生的人,而不是演奏和演唱机器,更不是相当有气派的大明星。当音乐响起,台上和台下都深深地沉浸在热爱之中,而不仅仅是一边表演,另一边看。

房东的猫
房东的猫,苏州
达闻西
达闻西乐队,上海
达达
达达乐队,杭州

再看去年定的目标们

  • 做好实验室项目(也就是 ChCore)的最后工作,为研究生生涯收尾
  • 做好暑期实习,保持学习新技术,并在秋招找到理想的正式工作
  • 更多地参与一些开源项目的贡献
  • 保持运动,让自己变得更健康
  • 读更多人文社科类的书,扩展自己的文化视野
  • 努力做到“己所不欲,勿施于人”

不可思议地,除了运动之外(也不是没运动,但从全年来看还是太少了),这些目标(我想)大概都算是完成了。不过想想毕竟目标其实都很笼统,也没啥完不成的。

不可思议
在上海博物馆拍到的“不可思议”

即将开始的 2023 年

新的一年,或许还是像去年一样定一些笼统的目标吧,这样在保持总体进步趋势的同时,可以允许自己在一年中对具体的计划有调整。

那么,希望自己可以在 2023 年,

  • 少输出观点,尤其避免在有争议的问题上输出不成熟的观点并意图说服别人,多输入,多思考,兼听则明;
  • 多旅游,亲身感受这个真实的世界;
  • 读更多书,包括一些文学、哲学、社会、西方政治历史相关的书;
  • 学习更多数据库知识,以更好地向 RisingWave 和可能的其它数据库开源项目贡献代码;
  • 继续或重新开始开发 rcOS;
  • 学习一些写代码之外的有趣技能;
  • 多运动。

就这样吧,结束这篇比去年冗长许多的年度总结,致新的人生~

2022,确实是一个新的开始

2022,会是一个新的开始吗

创建于
分类:Misc
标签:年度总结

像去年一样,拖到了农历新年之前才写过去一年的总结和新一年的展望。

从头至尾的失败

从去年农历新年开始算的话,刚一开始就遭遇了 rcOS 的搁浅,因为过年之前刚写完一部分,过年之后继续弄实验室的事情,一直到开学之后都没能抽出空继续写。

上半年那一学期,也就是研一下学期,课多、项目任务困难,从 3 月一直状态低沉到 6 月,期间甚至去做了心理咨询尝试解决。

后来确实通过各种沟通解决了心头的一些压力,包括撑完了所有课程、调整了实验室项目里分配给我的任务,结果紧接着,7 月份失恋了。从那开始一直到 11 月,感情上经历了各种跌宕起伏,已经不想也不便多说了。

年底开始好转

到年末,实验室项目中任务的参与感越来越强了,终于做到了自己感兴趣并擅长的事情,写代码的状态有所好转。

另一方面,感情方面的波折也渐渐放下了。同时,和许多老朋友和新朋友增进了一些交流,社交真的很能让人开心。

再到 2022 年的第一个月,社交、写代码、找实习都逐渐进入了舒适状态。尤其是一开始随手报的微软校招实习提前批最后居然面试通过了(这里顺便发一个 面经),让自信心提升了不少(虽然或许综合考虑后可能还是会选择去另一家公司)。

2022 年会更好吗?

目前看来今年的开头还是比较顺利的,希望之后可以越来越顺利,或至少平均而言保持和第一个月相当的状态吧。

总体目标大约如下:

  • 做好实验室项目(也就是 ChCore)的最后工作,为研究生生涯收尾
  • 做好暑期实习,保持学习新技术,并在秋招找到理想的正式工作
  • 更多地参与一些开源项目的贡献
  • 保持运动,让自己变得更健康
  • 读更多人文社科类的书,扩展自己的文化视野
  • 努力做到“己所不欲,勿施于人”
2022,会是一个新的开始吗

微软校招实习提前批面试经验

创建于
分类:Misc
标签:微软校招实习面试经验面经

部门选的是 C+AI,工作地点选的是上海。

1.17 一面

  • 中文自我介绍
    • 说得太长了,感觉面试官根本没听
  • 算法题:求一个未排序的数组的逆序对数量
    • 想了 40 分钟,期间让面试官提示了 3、4 次,愣是没想起来
    • 对归并排序、快排这类基础算法掌握不牢,也没复习到
    • 最后完全没做出来,没写出代码,也没想出思路
  • 表现稀烂,草草收场

1.18 二面

  • 中文自我介绍
    • 概括成了 4 句话左右,精炼了很多
  • 在实验室做的 OS 中负责什么内容
  • 算法题:原地倒转链表
    • 三指针,lastcurrnext
  • 顺着第一个算法题问了些 OS 知识
    • 代码是怎么编译出可执行文件的
      • 编译到目标文件,再链接为可执行文件
    • 可执行文件是怎么运行的
      • 我回答的是 OS 或 ld.so 加载 ELF 的过程
      • 实际想问的也可能是 fork + exec
  • 算法题:求一个 0-1 矩阵中最大的相邻 1 的面积
    • 假设所有相邻的 1 之间有边,用 DFS 或 BFS 找到最大连通子图即可
    • DFS 的遍历条件一开始写漏了,面试官提醒 2 次有问题让仔细检查一下,分别检查出了一些错,最后完成
  • 顺着第二个算法题问了些 OS 知识
    • vector<vector<int>> M 参数如果换成 int *Mint **M,访问元素是还是 M[i][j]
      • 这里主要是考察对一个指针进行 [] 实际会发生什么
    • 如果矩阵很大,DFS 这样的递归算法还能工作吗
      • 栈溢出
    • 什么因素影响递归的深度
      • OS 为线程分配的栈大小、以及是否会动态增加栈大小
      • 每层函数调用的栈帧大小,具体包括局部变量、caller/callee saved 寄存器、函数返回地址
    • 估算上面写的 DFS 函数每层调用消耗的栈空间
      • 我一开始忘记了 caller saved 寄存器(在 call 之前需要保存当前层的参数寄存器才能再设置下一层的参数),最后面试官说不太对准备结束了,我强行问他,然后才终于想起来

1.21 三面(Lead 面)

  • 中文介绍自己的教育背景和项目经历
  • 在实验室做的 OS 中负责什么内容
    • 问了微内核和 Linux 这种宏内核有什么区别
    • 问了我们的 OS 如何实现信号量
    • 还问了一些忘记了
  • 擅长什么编程语言
    • 答了 C++ 和 Python
    • 接着问 Python 的内存管理和 C++ 有什么区别
      • 想考察的是关于 GC 的知识,我一开始没反应过来,因为之前刚在说操作系统的内存管理
      • 后来我说 Python 的对象内存管理是通过 reference counting
      • 然后又说了 GC 的 mark-and-sweep 算法
      • 感觉这块答得非常混乱,太紧张了
  • 算法题:如何求一个数的平方根
    • 我说可以用牛顿迭代,但我有点忘了来牛顿迭代是怎么做的
    • 他说那想想别的方法
    • 最后用了二分法,要写出代码(就几行)
  • 算法题:给一个未排序的数组,如何找到数组排序后中间那个数(也就是第 n/2 大的数)
    • 只要求说思路
    • 实际上就是 top k 问题,用快排的思路,在 partition 的过程中根据 pivot 最后所在的位置和 n/2 比较,来决定继续处理左半边或右半边,直到 pivot 的位置就在 n/2
  • 算法题:扩展上一题,如果数组的数字是一个一个输入的,如何在输入每个数字之后输出当前第 n/2 大的数
    • 同样只要说思路
    • 一开始说不能把数字存下来,没想出解法(真的可能不把数字存下来吗😂)
    • 然后放宽要求,可以存下来,边想边跟面试官讨论,经历了以下思路
      • 维护一个平衡二叉树,让两个子树节点数相差不超过 1,后来我发现不可行
      • 然后赶紧提出最简单的思路,维护当前第 n/2 数和左右两边的链表,像插入排序那样,每来一个数就插入到对应的位置,当两边数量差超过 1 时,取出左边最大的或右边最小的,更新第 n/2 数
      • 接着面试官提醒左右两个链表是否要维护全序关系,我于是想到可以左右两边分别维护一个大顶堆和小顶堆即可
  • 上面每个算法题每个思路都问了复杂度
  • 最后问我有没有什么想问的,然后结束

1.29 录用意向书

最后在 1 月 29 号收到了录用意向书。

微软校招实习提前批面试经验

PIC、PIE 和 Copy Relocation

创建于
分类:Dev
标签:编译器链接器加载器GCCClangPICPIERelocation

这两天尝试修改 musl libc,碰到了个很怪的问题,最终找到了原因并解决,记录如下。

文章中部分表述不完全准确,请看 MaskRay 在评论区的补充~

奇怪的全局变量

起因是想在 libc 里添加一个全局变量 syscall_count 用来记录发生的 syscall 数量(这个需求本身很怪,因为只是在测试优雅地拦截 libc syscall 的方案)。具体地,添加了一个 C 文件如下:

// musl/src/syscall_count.c

int syscall_count;

int get_syscall_count()
{
    return syscall_count;
}

然后修改 arch/x86_64/syscall_arch.h 如下:

// musl/arch/x86_64/syscall_arch.h

// ...

extern int syscall_count;

static __inline long __syscall0(long n)
{
    syscall_count++;
    // ...
}

// ...

编译 libc 得到 libc.so 后,编写测试程序如下:

// test/main.c

#include <stdio.h>

extern int syscall_count;
extern int get_syscall_count();

int main(int argc, const char *argv[])
{
    printf("Hello World!\n");
    printf("main, syscall_count: %d\n", syscall_count);
    printf("main, get_syscall_count(): %d\n", get_syscall_count());
    return 0;
}

测试程序使用动态链接的方式编译链接(CMake 设置 CMAKE_C_COMPILER 为 musl libc 的 GCC wrapper musl-gcc 后的默认情况),输出的可执行文件类型为 dynamically linked shared object。

运行发现输出很不对劲:syscall_count 值为 5,而 get_syscall_count() 返回 9,中间只隔了一个 printf 居然多了 4 个 syscall。

察觉到奇怪后,加了些打印来 debug:

// musl/src/syscall_count.c

// ...

#include <stdio.h>

int get_syscall_count()
{
    printf("get_syscall_count, &syscall_count: %p, syscall_count: %d\n", &syscall_count, syscall_count);
    return syscall_count;
}
// test/main.c

// ...

int main(int argc, char const *argv[])
{
    printf("Hello World!\n");
    printf("main, &syscall_count: %p, syscall_count %d\n", &syscall_count, syscall_count);
    printf("main, get_syscall_count(): %d\n", get_syscall_count());
    printf("main, &syscall_count: %p, syscall_count %d\n", &syscall_count, syscall_count);
    return 0;
}

再同样方式构建运行输出如下:

Hello World!
main, &syscall_count: 0x562c67343008, syscall_count 5
get_syscall_count, &syscall_count: 0x7fdf59090fd4, syscall_count: 9
main, get_syscall_count(): 10
main, &syscall_count: 0x562c67343008, syscall_count 5

发现 test/main.clibc.so 中访问的 syscall_count 甚至地址都不一样。于是进行了一些尝试,给 test 程序添加了 -fPIC 编译选项,行为就符合预期了,改成加 -fPIE,行为又不正常(后来才发现我安装的 GCC 9 打开了 --enable-default-pie,也就是说 PIE 就是默认行为)。

虽然加上 -fPIC 后“解决了”问题(实际上并不是最正确的解法,后面说),但还是不甘心,因为最近被 PIC 相关问题折腾得够呛,想认真了解一下其中的细节,于是进行了一番搜索,找到了跟我相似的问题1,接着顺着问题下的回答和之前阅读 MaskRay(一位 LLVM 贡献者)的博客的印象,慢慢终于弄懂了问题产生的本质原因。

PIC 和 PIE

首先需要理解 PIC(Position Independent Code)和 PIE(Position Independent Executable)是怎么回事,这里只讨论 x86-64 架构的情形。

当不开 PIC 或 PIE 时,编译器假设目标程序最终会被加载到一个固定的虚拟地址,于是在生成访问全局变量和函数调用的指令时,如果无法使用 PC 相对寻址,则可以直接使用绝对地址寻址;而开了 PIC 或 PIE 后,编译器不知道目标程序运行时会加载到什么地址,因此需要使用 GOT(Global Offset Table)来间接寻址,等加载器加载 ELF 时,才在 GOT 表项中填充运行时的绝对地址。2

而 PIC 和 PIE 的区别则在于编译出来的目标文件的用途:PIC 模式编译出的目标文件可以被用于生成位置无关可执行文件或动态库,PIE 模式编译出的目标文件只能用于生成位置无关可执行文件,不能用于生成动态库(因此编译器有了一些优化空间)。当然,这里讨论的都是“位置无关”,位置相关的可执行文件可以从任何模式(PIC、PIE、no-PIC)编译的目标文件生成。3

Copy Relocation

接着就是我一开始遇到的问题的直接原因(根本原因在后面),也就是 copy relocation4

当使用 PIC 时,编译器为 syscall_count 变量使用了 GOT 间接寻址:

printf("main, &syscall_count: %p, syscall_count %d\n", &syscall_count, syscall_count);
1198:   48 8b 05 59 2e 00 00    mov    0x2e59(%rip),%rax        # 3ff8 <syscall_count>
119f:   8b 00                   mov    (%rax),%eax

因此在加载 libc.so 时给 GOT 表项填入了 syscall_count 的绝对地址,行为符合预期。

而使用 PIE 时,编译器为 syscall_count 使用了 PC 相对寻址:

printf("main, &syscall_count: %p, syscall_count %d\n", &syscall_count, syscall_count);
1198:   8b 05 6a 2e 00 00       mov    0x2e6a(%rip),%eax        # 4008 <syscall_count>

可是它并不知道 syscall_count 所在的 libc.so 会被加载到哪,怎么能 PC 相对寻址呢?答案就是编译器在测试程序 test 中为 syscall_count 进行了 copy relocation,创建了一份拷贝,通过 nm test/build/test 可以看出区别:

  • PIC 模式:
                 ...
                 U get_syscall_count
                 U syscall_count
  • PIE 模式:
                 ...
                 U get_syscall_count
0000000000004008 B syscall_count

可以看出 syscall_count 在 PIC 模式下标记为未定义符号(U),等待加载 libc.so 时进行 relocation,而 PIE 模式下直接被定义在了 BSS 段(B)。与此同时,get_syscall_count 在两种情况下都是未定义符号,也就是说会在运行时 relocate 到 libc.so 中的那一份,所以测试程序中直接访问 syscall_count 和调用 get_syscall_count 得到的结果不一致(这个解释在逻辑上还是有漏洞的,看下一节)。

通过 readelf -r test/build/test 可以更明确地看出 PIC 和 PIE 模式下编译器产生了不同的 relocation 行为5,进而印证上面的论断:

  • PIC 模式:
Relocation section '.rela.dyn' at offset 0x4a0 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
                                ......
000000003ff8  000800000006 R_X86_64_GLOB_DAT 0000000000000000 syscall_count + 0

Relocation section '.rela.plt' at offset 0x560 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
                                ......
000000003fc0  000200000007 R_X86_64_JUMP_SLO 0000000000000000 get_syscall_count + 0
  • PIE 模式:
Relocation section '.rela.dyn' at offset 0x4a8 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
                                ......
000000004008  000a00000005 R_X86_64_COPY     0000000000004008 syscall_count + 0

Relocation section '.rela.plt' at offset 0x568 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
                                ......
000000003fc8  000200000007 R_X86_64_JUMP_SLO 0000000000000000 get_syscall_count + 0

Dynamic Symbol

接着需要深究的是,既然 syscall_count 可能会被 copy relocation,那 libc.so 中的 get_syscall_count 访问 syscall_count 时不应该使用 GOT 间接寻址吗,这样才能保证一致性啊。然而查看 libc.so 的反汇编发现它用了 PC 相对寻址:

syscall_count++;
162ce:  48 8d 05 ff 3c 08 00    lea    0x83cff(%rip),%rax     # 99fd4 <syscall_count>
162d5:  ff 00                   incl   (%rax)

也就是说 libc.soget_syscall_count 用了自己 ELF 里定义的那份 syscall_count,这才最终导致测试程序访问 syscall_countget_syscall_count 的结果不一致。

libc.so 凭什么放心地对 syscall_count 进行 PC 相对寻址呢,这是因为 musl libc 在链接时通过 --dynamic-list=./dynamic.list 参数指定了 dynamic list(dynamic symbol table),意思是说,在这个表里面的符号,libc 会认为有可能在应用程序(可执行文件)中被重复定义,届时 libc 需要使用应用程序给出的定义。这个表里面包含了 malloc 等常见的允许被替换的函数和变量。6

如果不指定 dynamic list,则默认情况下链接器会认为所有符号都可能在应用程序中重复定义,会导致 libc 的性能开销显著增加(所有全局变量和函数访问都要经过 GOT),所以 musl libc 使用了 dynamic list。

因此,没有把 syscall_count 放到 dynamic.list 是我遇到上面问题的根本原因,一旦把它加进去,无论应用程序使用 PIC 还是 PIE 都能正确工作。

R_X86_64_REX_GOTPCRELX

在和 MaskRay 对 dynamic list 进行交流后,他提到 R_X86_64_REX_GOTPCRELX 这种 relocation 模式,学习后我明白了 libc.sosyscall_count 采用 PC 相对寻址的具体过程。

首先编译器把 .c 编译为目标文件时,为 syscall_count 产生了 R_X86_64_REX_GOTPCRELX relocation 模式,从汇编上可以看到此时指令还是像 GOT 间接寻址,需要两次访存:

syscall_count++;
119:    4c 8b 05 00 00 00 00    mov    0x0(%rip),%r8        # 120 <__init_libc+0x120>
12f:    41 ff 00                incl   (%r8)

接着在链接时,链接器发现 syscall_count 不在 dynamic list 中,于是对上面的指令进行优化7,最终在 libc.so 中产生如下指令,采用 PC 相对寻址:

syscall_count++;
16187:  4c 8d 05 46 3e 08 00    lea    0x83e46(%rip),%r8    # 99fd4 <syscall_count>
16191:  41 ff 00                incl   (%r8)

为什么 PIE 的行为和 PIC 不同?

说了这么多,仍然没有解释为什么 PIC 和 PIE 行为不同。我的理解是这样的,PIC 编译出的目标文件可能用于动态库,所以它不能对外部定义的全局变量有自己的拷贝,而是只能通过 GOT 表访问,而 PIE 则确定是用于可执行文件,所以即使它有自己的拷贝,只要所有动态库都通过 GOT 表访问,就能保证全局只有一个该变量。这就是前面说的,PIE 模式给了编译器一定的优化空间。

另外,PIE 模式下使用 copy relocation 也不是从一开始就有的行为,而是在 GCC 5 引入的,为的是减少使用 GOT 导致的额外内存访问开销8。Clang 也在某个版本中引入了编译选项 -mpie-copy-relocations 来开启 copy relocation910,后来 MaskRay 将其改成了 -f[no-]direct-access-external-data11,但后者在 GCC 的提议12没有被接受。

和 CMake 碰撞出的火花

在 CMake 中,当设置 CMAKE_POSITION_INDEPENDENT_CODEON 之后,它对于动态库会添加 -fPIC 选项,而对可执行文件会添加 -fPIE 选项。当 CMake 觉得自己充分利用了编译器优化时,实际上更悄无声息地触发了 copy relocation。

仍然迷惑的点

虽然上面已经大致搞明白了整个问题的原因,但我还是有一个疑惑的点,那就是 --dynamic-list 的语义到底是什么,网上看到的说法基本都是:在 dynamic list 中指定的符号,动态库链接时会认为潜在地可能在运行时由外部定义,于是不会绑定到动态库内的定义。但这个语义并没有说清楚,可执行文件通过 extern 声明(并没有显式地重新定义)并使用动态库中的全局变量会发生什么,而 copy relocation 正是利用了这个语义上的模糊地带,在 PIE 模式下隐式地在可执行文件中重新定义了动态库全局变量。

我个人认为 PIE 下默认进行 copy relocation 的行为是有问题的,当试图访问的变量不在 dynamic list 时应该报一个警告,或者维持原来的使用 GOT 的行为。而且,事实上在给测试程序添加 -fno-PIC 选项时,无论 syscall_count 在不在 libc.so 的 dynamic list 上,编译器都会为 syscall_count 产生 R_X86_64_PC32 relocation,进而在链接时报错,而不会进行 copy relocation,这才是符合逻辑的行为。

PIC、PIE 和 Copy Relocation

CMAKE_XXX_DIR 等 CMake 内置变量辨析

创建于
分类:Dev
标签:CMakeBuild System构建系统

最近高强度写 CMake 构建脚本(甚至还在实验室新人培训上 讲了一手),“再一次”搞清楚了 CMake 的 CMAKE_SOURCE_DIRCMAKE_CURRENT_SOURCE_DIR 等容易混淆的内置变量,记录并分享一下。

首先给出定义:

  • CMAKE_SOURCE_DIR:当前 CMake source tree 的顶层
  • CMAKE_BINARY_DIR:当前 CMake build tree 的顶层
  • CMAKE_CURRENT_SOURCE_DIR:当前正在处理的 source 目录
  • CMAKE_CURRENT_BINARY_DIR:当前正在处理的 binary 目录
  • CMAKE_CURRENT_LIST_FILE:当前正在处理的 CMake list 文件CMakeLists.txt*.cmake
  • CMAKE_CURRENT_LIST_DIR:当前正在处理的 CMake list 文件所在目录
  • PROJECT_SOURCE_DIR:当前最近的 project() 命令所在 source 目录
  • PROJECT_BINARY_DIR:当前最近的 project() 命令对应 binary 目录

下面通过例子来尝试解释清楚不同情况下,这些值都是什么。

现假设有一个项目名叫 cmake-playground,位于 /Users/richard/Lab/cmake-playground,有三个源码子目录 libserverclient 和一个 CMake 模块目录 cmake,整体目录结构如下:

.
├── client
│  ├── CMakeLists.txt
│  └── external
│     └── somelib
│        └── CMakeLists.txt
├── cmake
│  └── MyModule.cmake
├── CMakeLists.txt
├── lib
│  ├── CMakeLists.txt
│  ├── core
│  │  └── CMakeLists.txt
│  └── utils
│     └── CMakeLists.txt
└── server
   ├── CMakeLists.txt
   └── web
      └── CMakeLists.txt

基本情形

首先关注项目根目录和 lib 目录:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(Playground)

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

add_subdirectory(lib)
add_subdirectory(server)
add_subdirectory(client)
# lib/CMakeLists.txt

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

add_subdirectory(core)
add_subdirectory(utils)
# lib/core/CMakeLists.txt

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

使用如下命令进行 CMake configure 步骤:

$ cmake -S . -B build # -S 参数指定源码目录,-B 参数指定 build 目录

输出如下:

-- =================================
-- /Users/richard/Lab/cmake-playground/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================
-- =================================
-- /Users/richard/Lab/cmake-playground/lib/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/lib
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/lib
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================
-- =================================
-- /Users/richard/Lab/cmake-playground/lib/core/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/lib/core
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/lib/core
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================

可以发现 CMAKE_SOURCE_DIR 始终是项目根目录,也就是 CMake -S 参数指定的目录,CMAKE_BINARY_DIR 始终是 build 目录,也就是 -B 参数指定的目录。

随着 add_subdirectory 的深入,CMake 会设置 CMAKE_CURRENT_SOURCE_DIR 为当前正在处理的源码目录,同时,会在 CMAKE_BINARY_DIR 中创建对应层级的 build 目录,用于存放 CMake 产生的构建脚本(Makefile 等)和实际构建时产生的文件,并设置到 CMAKE_CURRENT_BINARY_DIR

PROJECT_SOURCE_DIRPROJECT_BINARY_DIR 这里等于 CMAKE_SOURCE_DIRCMAKE_BINARY_DIR,因为每次访问时,最近的 project() 调用都是在项目根目录 CMakeLists.txt

定义新的 Project

接着看 server 目录,这里通过 project() 调用定义了一个新的 project:

# server/CMakeLists.txt

project(MyServer)

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

add_subdirectory(web)
# server/web/CMakeLists.txt

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

同样通过 cmake -S . -B build 进行 configure,输出如下:

-- =================================
-- /Users/richard/Lab/cmake-playground/server/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/server
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/server
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/server
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/server
-- =================================
-- =================================
-- /Users/richard/Lab/cmake-playground/server/web/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/server/web
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/server/web
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/server
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/server
-- =================================

此时 PROJECT_SOURCE_DIRPROJECT_BINARY_DIR 变成了 server 对应的源码和 build 目录。

添加外部项目

看起来 CMAKE_SOURCE_DIRCMAKE_BINARY_DIR 永远是调用 CMake 命令时指定的源码和 build 目录,那它们有没有可能变呢?实际是有可能的。

这里 client 目录中使用 ExternalProject_Add 添加了一个外部项目:

# client/CMakeLists.txt

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

include(ExternalProject)
ExternalProject_Add(
    somelib
    SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/somelib
    INSTALL_COMMAND echo "Skipping install step")
# client/external/somelib/CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(SomeLib)

message(STATUS "=================================")
message(STATUS "${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

这次运行的 CMake 命令有所不同,因为 ExternalProject_Add 添加的外部项目要在外层项目 build 时才会 configure + build:

cmake -S . -B build && cmake --build build

输出如下(省略了不重要的内容):

-- =================================
-- /Users/richard/Lab/cmake-playground/client/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/client
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/client
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================
...
-- =================================
-- /Users/richard/Lab/cmake-playground/client/external/somelib/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground/client/external/somelib
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/client/somelib-prefix/src/somelib-build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/client/external/somelib
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/client/somelib-prefix/src/somelib-build
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/client/external/somelib
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/client/somelib-prefix/src/somelib-build
-- =================================

可以看到 CMake 为 somelib 项目创建了一个特别的目录 build/client/somelib-prefix(可以在 ExternalProject_Add 参数中配置),并在里面创建了 somelib 的 build 目录,然后设置到了 somelibCMAKE_SOURCE_DIRCMAKE_BINARY_DIR。这就像是为 somelib 创建了一个沙盒,让它不会干扰外层项目的构建环境。

在模块、宏、函数中

当上面这些变量遇到引入模块、调用宏和函数时,情况又变得更加复杂(当然这时候变量的作用域是更容易出错的点,先挖个坑,下次再写)。

我们首先编写一个 CMake 模块:

# cmake/MyModule.cmake

message(STATUS "=================================")
message(STATUS "CMAKE_CURRENT_LIST_FILE: ${CMAKE_CURRENT_LIST_FILE}")
message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message(STATUS "=================================")

macro(my_macro)
    message(STATUS "=================================")
    message(STATUS "In my_macro:")
    message(STATUS "CMAKE_CURRENT_LIST_FILE: ${CMAKE_CURRENT_LIST_FILE}")
    message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
    message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
    message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
    message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
    message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
    message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
    message(STATUS "=================================")
endmacro()

function(my_function)
    message(STATUS "=================================")
    message(STATUS "In my_function:")
    message(STATUS "CMAKE_CURRENT_LIST_FILE: ${CMAKE_CURRENT_LIST_FILE}")
    message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
    message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}")
    message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
    message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
    message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
    message(STATUS "PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
    message(STATUS "=================================")
endfunction()

然后在根目录 CMakeLists.txt 引入这个模块,并在之后的代码中使用其中的宏和函数:

# CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(Playground)

set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(MyModule)

# ...
# lib/CMakeLists.txt

my_macro()
my_function()

# ...

执行 configure,输出如下:

-- =================================
-- CMAKE_CURRENT_LIST_FILE: /Users/richard/Lab/cmake-playground/cmake/MyModule.cmake
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================
-- =================================
-- In my_macro:
-- CMAKE_CURRENT_LIST_FILE: /Users/richard/Lab/cmake-playground/lib/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/lib
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/lib
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================
-- =================================
-- In my_function:
-- CMAKE_CURRENT_LIST_FILE: /Users/richard/Lab/cmake-playground/lib/CMakeLists.txt
-- CMAKE_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- CMAKE_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- CMAKE_CURRENT_SOURCE_DIR: /Users/richard/Lab/cmake-playground/lib
-- CMAKE_CURRENT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build/lib
-- PROJECT_SOURCE_DIR: /Users/richard/Lab/cmake-playground
-- PROJECT_BINARY_DIR: /Users/richard/Lab/cmake-playground/build
-- =================================

也就是说,当 include(MyModule) 时,CMAKE_CURRENT_LIST_FILECMAKE_CURRENT_LIST_DIR 指向 CMake 模块文件的位置,而其它相关变量则继承自 include 的调用处;当调用宏和函数时,上述所有变量都继承自调用处。

所以应该用哪个呢?

这里记录几个我自身的经验:

  • 如果要引用同目录或下级目录的位置,可以用相对路径或使用 CMAKE_CURRENT_SOURCE_DIR/CMAKE_CURRENT_LIST_DIR(根据当前 CMake 文件是 CMakeLists.txt 还是 *.cmake 适当选择)
  • configure_file 产生的文件存放位置和 add_custom_target 默认的工作目录是 CMAKE_CURRENT_BINARY_DIR
  • 当要引用相对于项目根目录的位置时
    • 如果是在编写一个库,应该用 PROJECT_SOURCE_DIR,防止别人把你的项目当作子目录去 add_subdirectory 时出现问题
    • 如果是在编写一个应用,或者确定项目不会被 add_subdirectory,可以使用 CMAKE_SOURCE_DIR
CMAKE_XXX_DIR 等 CMake 内置变量辨析