Project RC

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

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 内置变量辨析

对开源的一些感悟

创建于
分类:Misc
标签:开源Open Source软件开发CQHTTP

通过 CQHTTP(最初的全称叫 CoolQ HTTP API),我经历的不只是一个项目的开发、迭代和流行,而是一整个以 CQHTTP 为核心的开源社区的形成,这里面包括各语言 SDK、开发框架、具体的机器人应用、web 控制面板、API 兼容的替代实现、CoolQ 关闭服务之后的替代 runtime、各类教程和文档等。

有时候我觉得这个过程和 Linus Torvalds 上传 Linux 初版代码之后逐渐形成 Linux 社区没什么本质的不同,只是规模小了几个数量级、场景更加小众。

在这个社区的形成过程中,很重要的一点是,CQHTTP 和大部分周边项目都是开源的,开源的关键在于“开放源代码”(open source),而不只是“公开源代码”(public source)。“开放”的意思是说,首先,社区是开放的,任何人都可以参与到社区里来,开发各式各样的周边项目,其次,这些项目本身也是开放的,从项目的一开始就以开源的方式进行,作者在 GitHub 上公开 push 代码,同时也随时可接受别人的 pull request。与之相对的,是另一种我想称之为“假开源”的形式,在项目封闭开发完成后,去掉所有 Git 历史,创建新的第一个 commit,内容写做“release 1.0.0”,然后发布到代码托管网站,接着可能在 issue 里面回答一些问题,代码仍然封闭开发,到下一版完成后,再去掉 Git 历史,覆盖到上一版代码中,提交第二个 commit,写做“release 1.1.0”。这后一种“开源”,只是把源代码公开了,而不是真正的开放。

另一个很重要的点,是所有项目的开发者,都是自发地来参与开发,而不是为了钱或是被迫的。每个人都是出于自身的兴趣,选择从头开发或者参与社区中的已有项目,没有人需要被安排“你这周做什么,下个月做什么”,也没有人期待从项目中获得金钱收益。大家主动从社区中发现需要修复的 bug、需要补充的功能,而不是被动地做自己不愿意做的事。如果你要问这些开发者为什么作业那么多、上课那么忙,还要熬夜写这些代码,大部分会回答“因为好玩”。

从初中生、高中生到大学生,甚至是工作后半路转行做程序员的人们,有很多人在使用 CQHTTP 社区项目的过程中入门了编程,无论是 C++、Python 或是其它。也有很多人通过开发 CQHTTP 社区项目获得了自己的用户群体、GitHub 星星、更丰富的编程经验和更广的人脉,并以此找到了更好的学习和工作机会,包括我自己。这常常让我感慨万分,让我感到自己曾经做的事情并不是简单的玩具,虽然它的影响范围很小,但它是有意义的,它帮助到了许多人,也或许间接地微弱地帮助到了中国的软件行业。

我想我会一直坚持我所认同的开源理想主义,坚持我的初心。

当然了,开源的开发形式和理想主义并不是 CQHTTP 本身成功的根本原因,它成功的根本原因是解决了 CoolQ 难以用多种编程语言开发的痛点。如果当时我选择闭源,它可能仍然会成功。但如果真的闭源,可能就很难形成后来那么开放的社区了。所以,要想成功,解决痛点是必须的,而解决痛点之后以什么形式呈现,就是理想主义和现实主义的分野了。

对开源的一些感悟