为了使源码可编译,首先需要预处理器(preprocessor)处理 #include#define 等命令(directive)并移除注释。但是常常会有一些头文件找不到或 typedef 未知的奇怪问题。

一个预处理的简单例子

例如,我们要编译 python-ldap 的 C 扩展模块。作为 Python 项目整体打包时,我们只需要执行 python setup.py install,而从 setup.py 知道扩展模块包含的源码和头文件之后,直接调用 C 编译器单独预处理这部分 C 代码却不成功:

以 gcc 为例(clang 和 cpp 命令类似)

gcc -E <file.c>

首先一个问题就是 'Python.h' file not found,这是 Python 外部函数接口 Python/C API 定义的入口,而 C 编译器并不知道 Python 头文件的位置。所以我们用 -I <dir> 参数指定一个 include 搜索路径:

gcc -E -I<3.6/include/python3.6m> file.c

具体路径可以用 find / -name 'Python.h' 搜索,同理补全更多的搜索路径之后就可以通过编译器的预处理阶段了。

解析预处理的输出

预处理不是目的,一般还需要转换到某些中间形式才能进行静态分析,以使用 pycparser(或 libclang)生成 AST 为例,在解析(parse)上一步得到的预处理后的代码时,往往又会产生新的报错,例如系统头文件 <MacOSX.sdk/usr/include/i386/_types.h><MacOSX.sdk/usr/include/lber.h> 中一些 typedef 未知,报错形如 before: __attribute__before: __OSX_AVAILABLE_BUT_DEPRECATED_MSG。这里有两个问题,其一是这些头文件并不在我们的搜索路径中,这是因为 gcc 有一些预置的系统头文件目录,使用编译参数 -nostdinc 可以禁用这种行为。其二是既然预处理器没有报错,为什么其输出无法解析,这是因为 pycparser 不支持 GCC 等编译器扩展,比如 GNU-specific __attribute__,为此我们可以使用 -D 参数补充这块定义,比如 -D'__attribute__(x)='

pycparser 使用 fake_libc_include 处理一些标准 C 库头文件,其中给出一些 #definetypedef 的最小化定义:

typedef int T;

即解析器(parser)只需要知道存在类型 T,而不在乎该类型的具体实现(语义),比如 T 可能是一个接受结构体数组的函数指针。

pycparser 支持完整的 C99 语言,如果目标程序只依赖标准 C 库头文件(standard C library headers,C runtime,如 stdio.h),那么 -I<fake_libc_include> 一般就够了。但是如前例所示,目标程序可能还依赖系统头文件(system headers,如 _types.h)和其他库的头文件(other library headers,如 Python.h),这些就需要通过编译参数补充,这些补充定义也可以是最小化的。一般来说,对于我们关心的部分(假设是目标程序和 Python/C API)应该提供完整定义,而其他部分就可以使用最小化定义,这样既可以省去大量繁琐工作,也可以减少解析时间和 AST 体积。

一个复杂的例子

我们尝试预处理并解析 Python 源码中 Modules 目录下 C 语言实现的标准库,按照以上策略补充了一系列参数之后依旧存在两个问题,其一是一些定义和平台产生了冲突(如 definition wrong for platform)。其二是补充定义的量过大而无法穷尽,一般是存在大量系统相关的定义(如 __declspec__asm__),这里其实已经变成了一个交叉编译(cross compilation)的问题(TODO)。

一个简单的替代解决方法是先在本地机器上执行构建(build)Python 的配置脚本:

./configure --with-pydebug

即产生平台相关的 pyconfig.h 来解决上述问题,之后重复以上策略配置少量参数就可以得到目标代码的 AST 了。

一个小工具

一方面,有一些通用的编译参数,也有一些目标项目特定的,对于同一项目希望可以实现参数的复用,同时不直接修改而是复用 fake_libc_include;另一方面,希望配置的过程可以遵循一定的流程,避免输入非常长的命令,并且不和分析部分代码耦合。因此,基于 pycparser 和 yaml 实现了一个编译参数配置工具,可以自动解析配置文件中的编译参数,动态构建 pycparser.parse_file() 的参数列表。

主程序 preprocess 除目标文件路径外还接受一个可选参数。缺省时仅加载配置文件 pp_config.ymlall 对象存储的通用参数,指定 <project_name> 时则会加载项目特定参数。调试过程就是反复执行主程序,根据报错信息增量地修改配置文件中对应对象的参数条目。调试成功后输出 C 目标程序的 AST,编译参数也自动在配置文件中保留了下来。

参考

  1. pycparser
  2. On parsing C, type declarations and fake headers
  3. How to preprocess a C source code for pycparser
  4. 用 __attribute__((deprecated)) 管理过时的代码
  5. __attribute__ 总结
  6. Include/pyport.h: Bad LONG_BIT assumption on non-glibc sys
  7. OSXCross: Fix building GCC with 10.15 SDK, __OSX_AVAILABLE_BUT_DEPRECATED_MSG
  8. Microsoft-specific modifiers: __declspec
  9. What does __asm__ __volatile__ do in C
  10. Build Python from source: 1, 2, 3
  11. yaml