# 符号冲突影响程序运行的正确性
这里我以一个例子说明这个问题。工程结构如下:
example
├── foo
│ ├── call_lib_foo.c
│ ├── call_lib_foo.h
│ ├── foo.c
│ └── foo.h
├── main.c
├── my_foo.c
└── my_foo.h
foo
的目录有两个文件,内容分别为:
// foo.c
void foo(void) {
printf("foo in lib\n");
}
// call_lib_foo.c
void call_lib_foo(void) {
foo();
}
构建命令如下:
clang -c foo/foo.c -o build/foo/foo.o
clang -c foo/call_lib_foo.c -o build/foo/call_lib_foo.o
ar rcs build/libfoo.a build/foo/foo.o build/foo/call_lib_foo.o
这将为 foo
目录下两个文件编译打包成一个静态库 libfoo.a
。
根目录内容如下:
// my_foo.c
void foo(void) {
printf("foo in main\n");
}
// main.c
int main(int argc, const char * argv[]) {
foo();
call_lib_foo();
return 0;
}
构建命令如下:
clang -c my_foo.c -o build/my_foo.o
clang -c main.c -o build/main.o
clang build/my_foo.o build/main.o -lfoo -Lbuild -o build/example
在 main
函数中,我们先后调用了 foo
和 call_lib_foo
,执行后预期输出内容如下:
$ ./build/example
foo in main
foo in lib
预期调用关系为:
main -> foo(main)
main -> call_lib_foo -> foo(lib)
但实际执行结果为:
$ ./build/example
foo in main
foo in main
在 call_lib_foo
中错误地调用了主工程的 foo
函数,这是因为静态链接器在解决符号定义和引用关系时,每个符号只会加载一个。在例中,选择了主工程的 foo
函数。
这是一个比较常见的情况,多出现在使用外部闭源组件场景,当闭源组件内部使用了一份开源组件的实现,同时我们也用到了该开源组件。如果两份组件因为定制化、版本不同,出现部分实现不同,最终将发现如本例一样的情况,调用了和预期不一致的函数。
由于我们无法修改源码,为冲突的函数添加一个前缀区分开,导致这个问题变得很棘手。我们可以有以下几种策略:
- 修改我们使用开源组件实现,函数换个名字
- 将闭源组件转换为动态库集成
- 直接使用闭源组件内置的实现
事实上我们还有一种解决方式:直接修改 Mach-O,改变闭源组件的符号,避免遇到上述冲突。
# 修改 Mach-O 符号信息
这里简化了很多内容,关于 Mach-O 详细内容可以参阅其他文章。
以 call_lib_foo.o
为例:
LC_SYMTAB
(.symtab
)记录了 Symbol Table 和 String Table 的位置,以及他们的个数和大小。
这将是我们要重点关心的内容。对应的结构体如下:
定义在 mach-o/loader.h
:
/*
* The symtab_command contains the offsets and sizes of the link-edit 4.3BSD
* "stab" style symbol table information as described in the header files
* <nlist.h> and <stab.h>.
*/
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
不论指令跳转到指定函数,还是重定位表,引用的符号信息都记录在 Symbol Table 中,我们只要找到合适的修改方式即可。
foo
函数的符号信息如下:
对应的结构体定义在 mach-o/nlist.h
中,内容如下:
/*
* This is the symbol table entry structure for 64-bit architectures.
*/
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
可以看到 n_un.n_strx
记录的是符号的字符串表示形式的位置,_foo
符号在 String Table 的 0x0000000A
位置,即第 10
个索引。对应内容如下:
String Table 使用空字符分割不同的字符串,用来表示该字符串读取完毕。
这里是从 5F
到 6F
,即 5F 66 6F 6F
,刚好对应 _foo
。
由于
_foo
刚好是_call_lib_foo
的子串,Mach-O 为了节省空间,直接放到一起了。
从这里可以看到,我们将符号表中的 String Table 的索引指向一个新字符串即可。
替换流程如下:
- 读取 Mach-O 文件,先找到
LC_SYMTAB
信息,确定 Symbol Table 和 String Table 信息 - 遍历 Symbol Table 找到要修改的符号
- 在 String Table 添加新符号的字符串
- 将对应的符号记录的索引改为新字符串的起始位置
- 最后不要忘了修改 String Table 的
symtab_command.strsize
大小
供参考的示例代码参见:https://github.com/DianQK/change-mach-o-symbol (opens new window)。
示例代码每次仅支持修改单个 Mach-O 文件的一个符号,你可以扩展成直接修改一个静态库的形式。
参考内容: