Draft
[WIP]【PR】: LD_PRELOAD问题定位及测试 #4594
[WIP]【PR】: LD_PRELOAD问题定位及测试 #4594
Draft
李力_LeonAdd on Aug 29
Total: 2 files changed+414-0
@@ -0,0 +1,135 @@
1+# API Hook LD_PRELOAD 劫持方案说明
2+ 
3+## 问题背景
4+ 
5+### Hook 需求前的状态:LD_PRELOAD 劫持可以生效
6+ 
7+在做 API Hook 需求之前,`libacl_rt.so` 中 `aclrtGetSocName` 等接口通过 `ACL_RT_CPP` 宏直接转发:`ret name sig { return name##Impl args; }`。这个 `name##Impl` 调用是 `libacl_rt.so` 对 `libruntime.so` 中 `*Impl` 函数的**外部符号引用**(`U` 符号)。
8+ 
9+由于 `libacl_rt.so` 虽然也编译了 `-Bsymbolic`,但 `*Impl` 在 `libacl_rt.so` 中是未定义(`U`)的,`-Bsymbolic` 只影响本地有定义的符号。因此动态链接器在解析这个 `U` 符号时走全局搜索顺序,`LD_PRELOAD` 库优先命中,劫持可以生效。
10+ 
11+### Hook 需求引入后:劫持失效
12+ 
13+API Hook 机制通过 `ACL_HOOK_DEF` 宏定义 `g_hook_*` 全局变量,存储 `*Impl` 函数指针(`originalFunc` 和 `currentFunc`)。`ACL_RT_CPP_HOOKABLE` 转发函数通过 `g_hook_*.currentFunc` 间接调用,实现 Hook 替换。
14+ 
15+原始实现中,`g_hook_*` 定义在 `dfx.cpp`,编译进 `libruntime.so`。此时 `&name##Impl` 的取地址操作发生在 `libruntime.so` **内部**——而 `*Impl` 函数也在 `libruntime.so` 中定义(`T` 符号),且 `libruntime.so` 编译了 `-Bsymbolic`,导致符号解析绑定到本地实现,`LD_PRELOAD` 劫持失效。
16+ 
17+## 原因分析
18+ 
19+### `-Bsymbolic` 机制详解
20+ 
21+`-Wl,-Bsymbolic` 是链接器选项,作用是在生成的 SO 的 `.dynamic` 段中设置 `DT_SYMBOLIC` 标志。该标志改变动态链接器解析该 SO 内部符号引用的顺序。
22+ 
23+**默认行为(无 `-Bsymbolic`)**:SO 内部引用一个全局可见符号时,动态链接器按**全局搜索顺序**解析。全局搜索顺序为:可执行文件 → `LD_PRELOAD` 库(按顺序)→ 已加载的 SO(按加载顺序)。第一个提供该符号定义的对象胜出。
24+ 
25+**`-Bsymbolic` 行为**:动态链接器在解析该 SO 内部的符号引用时,**优先搜索自身的符号表**。如果该 SO 本身有该符号的定义(`T`/`D`/`B` 等),直接绑定到本地定义,**不会继续搜索全局作用域**。只有本地找不到(`U` 符号)时,才回退到全局搜索顺序。
26+ 
27+**对 `LD_PRELOAD` 的影响**:`LD_PRELOAD` 的劫持能力依赖全局搜索顺序——劫持库的符号定义排在被劫持 SO 之前。但 `-Bsymbolic` 使 SO 内部的符号引用绑定到本地,跳过了全局搜索,`LD_PRELOAD` 库的符号定义被忽略。
28+ 
29+**对 `U` 符号的影响**:`-Bsymbolic` 只影响 SO **本地有定义**的符号。对于 SO 中未定义(`U`)的符号引用,本地搜索找不到,动态链接器正常走全局搜索顺序,`LD_PRELOAD` 仍然可以劫持。
30+ 
31+**使用原因**:`-Bsymbolic` 的主要好处是减少符号解析开销(避免运行时全局搜索)和提高符号隔离性(防止同一进程中被其他 SO 的同名符号意外覆盖)。`libruntime.so` 和 `libacl_rt.so` 都使用了此选项。
32+ 
33+### `-Bsymbolic` 阻止 LD_PRELOAD 劫持
34+ 
35+`libruntime.so` 编译时使用了 `-Wl,-Bsymbolic`(`runtime.cmake:684`),设置 `DT_SYMBOLIC` 标志。
36+ 
37+`-Bsymbolic` 的效果:动态链接器在解析该 SO 内部的符号引用时,**优先搜索自身的符号表**。如果本地有定义,直接绑定,不会继续搜索全局作用域(包括 LD_PRELOAD 库)。
38+ 
39+### 原始方案的符号流向
40+ 
41+```
42+libruntime.so:
43+ - 定义 g_hook_* (ACL_HOOK_DEF 在 dfx.cpp 中展开)
44+ - 定义 *Impl 函数 (aclrtGetSocNameImpl 等,ACL_RT_IMPL_HEADER 声明)
45+ - 编译选项: -Bsymbolic
46+ 
47+libacl_rt.so:
48+ - 引用 g_hook_* (ACL_RT_CPP_HOOKABLE 转发函数引用)
49+ - 引用 *Impl 函数 (通过 g_hook_*.currentFunc 间接调用)
50+```
51+ 
52+`g_hook_*` 在 `libruntime.so` 内部静态初始化时,`&name##Impl` 解析过程:
53+ 
54+1. `-Bsymbolic` 生效 → 优先搜索 `libruntime.so` 自身符号表
55+2. 找到本地 `aclrtGetSocNameImpl` 定义 → 直接绑定
56+3. **不会搜索全局作用域** → LD_PRELOAD 的劫持函数被跳过
57+ 
58+结果:`g_hook_*.originalFunc` 和 `currentFunc` 始终指向 `libruntime.so` 本地的 `*Impl`,`aclrtGetSocName()` 调用走的是原始实现,劫持无效。
59+ 
60+### `LD_DEBUG=bindings` 验证
61+ 
62+```
63+binding file libruntime.so [0] to libruntime.so [0]: normal symbol 'aclrtGetSocNameImpl' ← 静态初始化绑定到本地
64+binding file ./test_real [0] to ./libhijack_real.so [0]: normal symbol 'aclrtGetSocNameImpl' ← 测试程序的dlsym绑定到劫持库
65+```
66+ 
67+两个绑定地址不同,证实 `-Bsymbolic` 导致 `libruntime.so` 内部的符号解析未命中 LD_PRELOAD。
68+ 
69+## 修改方案
70+ 
71+### 核心思路
72+ 
73+将 `g_hook_*` 定义从 `libruntime.so`(有 `-Bsymbolic` 且定义 `*Impl`)移到 `libacl_rt.so`(有 `-Bsymbolic` 但**不定义** `*Impl`)。
74+ 
75+`libacl_rt.so` 中 `*Impl` 是 `U`(未定义引用),`-Bsymbolic` 只影响本地有定义的符号,对 `U` 符号无效。符号解析回退到全局作用域,LD_PRELOAD 优先命中。
76+ 
77+### 修改后的符号流向
78+ 
79+```
80+libacl_rt.so:
81+ - 定义 g_hook_* (ACL_HOOK_DEF 移到 acl_rt.cpp,弱定义 V)
82+ - 引用 *Impl 函数 (*Impl 是 U 符号,-Bsymbolic 不影响)
83+ - 编译选项: -Bsymbolic(对 U 符号无效)
84+ 
85+libruntime.so:
86+ - 弱引用 g_hook_* (ACL_HOOK_DECL 加 __attribute__((weak)),w 符号)
87+ - 定义 *Impl 函数 (不变)
88+ - 编译选项: -Bsymbolic(不影响,g_hook_* 不在此定义)
89+```
90+ 
91+`g_hook_*` 在 `libacl_rt.so` 内部静态初始化时,`&name##Impl` 解析过程:
92+ 
93+1. `-Bsymbolic` 生效 → 搜索 `libacl_rt.so` 自身符号表
94+2. `*Impl` 是 `U`(未定义)→ **本地找不到**
95+3. 回退到全局作用域搜索
96+4. LD_PRELOAD 库优先 → 命中劫持函数
97+ 
98+### 弱符号处理
99+ 
100+`dfx.cpp` 中的 `g_aclrtApiLookup[]` 查找表引用 `&g_hook_*`。`g_hook_*` 定义移到 `libacl_rt.so` 后,`libruntime.so` 中的这些引用变为未定义符号。如果直接用 `extern` 强引用,`libruntime.so` 独立加载(`dlopen`)时会因找不到 `g_hook_*` 而失败。
101+ 
102+通过将 `ACL_HOOK_DECL` 的 `extern` 声明加 `__attribute__((weak))`,使 `libruntime.so` 中的 `g_hook_*` 引用变为弱引用:
103+ 
104+- **`libacl_rt.so` 在场时**(正常使用场景):弱引用自动解析到 `libacl_rt.so` 中的强定义,hook 功能正常。
105+- **`libruntime.so` 独立加载时**:弱引用解析为 NULL,`FindHookEntryByName` 跳过 NULL entry 返回 nullptr,`aclrtApiInjectionSetFunc/GetFunc` 报参数错误。hook 功能不可用,但 `rtGetSocVersion` 等原生接口正常工作。
106+ 
107+### 具体改动
108+ 
109+共修改 3 个文件:
110+ 
111+1. **`src/acl/aclrt/acl_rt.cpp`**:新增 `ACL_HOOK_DEF` 宏展开,使 `g_hook_*` 定义编译进 `libacl_rt.so`。
112+ 
113+2. **`src/acl/aclrt_impl/dfx.cpp`**:删除 `ACL_HOOK_DEF` 宏展开,`g_hook_*` 不再在 `libruntime.so` 中定义。`FindHookEntryByName` 遍历时跳过 `entry == nullptr` 的项(弱符号未解析时)。
114+ 
115+3. **`src/acl/aclrt_impl/acl_rt_wrapper.h`**:
116+ - `ACL_HOOK_DECL` 加 `__attribute__((weak))`,使 `libruntime.so` 中的 `g_hook_*` 引用为弱引用,支持独立加载。
117+ - `ACL_HOOK_DEF` 宏恢复使用 `&name##Impl` 直接取地址(之前的 `dlsym` 方案不再需要),并删除不再使用的 `#include <dlfcn.h>`。
118+ 
119+### 不需要修改的文件
120+ 
121+| 文件 | 原因 |
122+|------|------|
123+| `runtime.cmake` / `tiny.cmake` / `cmodel.cmake` | `dfx.cpp` 仍在源列表中,只是代码减少。`ACL_RT_API_HOOK_ENABLE` 保留无害(`extern` 声明不产生引用) |
124+| `arch5162.cmake` | arch5162 不启用 `ACL_RT_API_HOOK_ENABLE`,`dfx.cpp` 的 hook 代码被 `#ifdef` 排除,搬走后效果一致 |
125+ 
126+## 验证
127+ 
128+整包编译后 `nm -D` 确认符号分布:
129+ 
130+```
131+libacl_rt.so: 0000000000030458 V g_hook_aclrtGetSocName ← 弱定义在 libacl_rt.so
132+libruntime.so: w g_hook_aclrtGetSocName ← 弱引用
133+```
134+ 
135+`LD_PRELOAD` 劫持测试通过,`dlopen("libruntime.so")` 独立加载也正常,详见 `docs/api_hook_preload_test.md`。
@@ -0,0 +1,279 @@
1+# LD_PRELOAD 劫持测试报告
2+ 
3+## 测试目标
4+ 
5+验证 `ACL_HOOK_DEF` 中 `g_hook_*` 定义移至 `libacl_rt.so` 后,`LD_PRELOAD` 劫持 `*Impl` 函数是否生效。
6+ 
7+## 测试环境
8+ 
9+- 平台:aarch64-linux
10+- 编译命令:`bash build.sh --pkg -j8`(整包编译)
11+- 安装路径:`/tmp/opencode/preload_test/real_runtime5/cann`
12+ 
13+## 测试文件
14+ 
15+### 劫持库 `libhijack_real.so`
16+ 
17+源文件 `libhijack_real.c`:
18+ 
19+```c
20+#include <stdio.h>
21+#include <string.h>
22+ 
23+const char *aclrtGetSocNameImpl(void)
24+{
25+ printf(" [PRELOAD] aclrtGetSocNameImpl hijacked!\n");
26+ return "Ascend-HIJACKED-BY-PRELOAD";
27+}
28+```
29+ 
30+编译命令:
31+ 
32+```bash
33+gcc -shared -fPIC -o libhijack_real.so libhijack_real.c
34+```
35+ 
36+### 测试程序 `test_real`
37+ 
38+源文件 `test_real.c`:
39+ 
40+```c
41+#include <stdio.h>
42+#include <dlfcn.h>
43+#include <string.h>
44+#include "acl/acl_base_rt.h"
45+ 
46+typedef const char *(*getSocName_t)(void);
47+ 
48+typedef struct {
49+ void *originalFunc;
50+ void *currentFunc;
51+} apiEntry_t;
52+ 
53+int main(void)
54+{
55+ printf("=== 真实Runtime LD_PRELOAD劫持测试 ===\n\n");
56+ 
57+ // 1. 检查 dlsym 能否找到 Impl 函数
58+ printf("1. dlsym 查找 aclrtGetSocNameImpl:\n");
59+ dlerror();
60+ void *sym = dlsym(RTLD_DEFAULT, "aclrtGetSocNameImpl");
61+ const char *err = dlerror();
62+ if (err != NULL) {
63+ printf(" dlsym error: %s\n", err);
64+ } else {
65+ printf(" dlsym result = %p\n", sym);
66+ }
67+ 
68+ // 2. 查找 g_hook_aclrtGetSocName 结构体
69+ printf("\n2. dlsym 查找 g_hook_aclrtGetSocName:\n");
70+ dlerror();
71+ apiEntry_t *hook = (apiEntry_t *)dlsym(RTLD_DEFAULT, "g_hook_aclrtGetSocName");
72+ err = dlerror();
73+ if (err != NULL) {
74+ printf(" dlsym error: %s\n", err);
75+ } else {
76+ printf(" g_hook_aclrtGetSocName = %p\n", (void *)hook);
77+ printf(" originalFunc = %p\n", hook->originalFunc);
78+ printf(" currentFunc = %p\n", hook->currentFunc);
79+ if (hook->originalFunc == sym) {
80+ printf(" -> originalFunc 与 dlsym 结果一致\n");
81+ } else {
82+ printf(" -> originalFunc 与 dlsym 结果不同!\n");
83+ }
84+ }
85+ 
86+ // 3. 直接调用 aclrtGetSocName(走 libacl_rt.so 的 ACL_RT_CPP_HOOKABLE 转发函数)
87+ printf("\n3. 直接调用 aclrtGetSocName()(走libacl_rt.so转发):\n");
88+ const char *result = aclrtGetSocName();
89+ printf(" 返回值: \"%s\"\n", result);
90+ 
91+ if (strcmp(result, "Ascend-HIJACKED-BY-PRELOAD") == 0) {
92+ printf(" -> LD_PRELOAD 劫持成功! dlsym方式生效!\n");
93+ } else {
94+ printf(" -> 未被劫持,返回真实值\n");
95+ }
96+ 
97+ // 4. 对比:直接 dlsym 调用 Impl
98+ printf("\n4. 直接调用 aclrtGetSocNameImpl()(通过dlsym):\n");
99+ if (sym != NULL) {
100+ getSocName_t implFn = (getSocName_t)sym;
101+ const char *result2 = implFn();
102+ printf(" 返回值: \"%s\"\n", result2);
103+ if (strcmp(result2, "Ascend-HIJACKED-BY-PRELOAD") == 0) {
104+ printf(" -> dlsym 直接调用也被劫持!\n");
105+ } else {
106+ printf(" -> dlsym 直接调用未被劫持\n");
107+ }
108+ }
109+ 
110+ printf("\n=== 测试结束 ===\n");
111+ return 0;
112+}
113+```
114+ 
115+编译命令:
116+ 
117+```bash
118+RT_LIB=/tmp/opencode/preload_test/real_runtime5/cann/lib64
119+gcc -o test_real test_real.c -ldl \
120+ -I/tmp/opencode/preload_test/real_runtime5/cann/include \
121+ -L$RT_LIB -lacl_rt \
122+ -Wl,-rpath,$RT_LIB -Wl,--no-as-needed
123+```
124+ 
125+## 测试步骤
126+ 
127+### 步骤 1:整包编译
128+ 
129+```bash
130+bash build.sh --pkg -j8
131+```
132+ 
133+### 步骤 2:安装到临时目录
134+ 
135+```bash
136+mkdir -p /tmp/opencode/preload_test/real_runtime5
137+bash build_out/cann-npu-runtime_9.2.0_linux-aarch64.run \
138+ --install-path=/tmp/opencode/preload_test/real_runtime5 --full --quiet
139+```
140+ 
141+### 步骤 3:编译劫持库
142+ 
143+```bash
144+gcc -shared -fPIC -o libhijack_real.so libhijack_real.c
145+```
146+ 
147+### 步骤 4:编译测试程序
148+ 
149+```bash
150+RT_LIB=/tmp/opencode/preload_test/real_runtime5/cann/lib64
151+gcc -o test_real test_real.c -ldl \
152+ -I/tmp/opencode/preload_test/real_runtime5/cann/include \
153+ -L$RT_LIB -lacl_rt \
154+ -Wl,-rpath,$RT_LIB -Wl,--no-as-needed
155+```
156+ 
157+### 步骤 5:检查符号分布
158+ 
159+```bash
160+RT_LIB=/tmp/opencode/preload_test/real_runtime5/cann/lib64
161+nm -D $RT_LIB/libacl_rt.so | grep g_hook_aclrtGetSocName
162+nm -D $RT_LIB/libruntime.so | grep g_hook_aclrtGetSocName
163+```
164+ 
165+### 步骤 6:不带 LD_PRELOAD 运行
166+ 
167+```bash
168+RT_LIB=/tmp/opencode/preload_test/real_runtime5/cann/lib64
169+LD_LIBRARY_PATH=$RT_LIB:$LD_LIBRARY_PATH ./test_real
170+```
171+ 
172+### 步骤 7:带 LD_PRELOAD 运行
173+ 
174+```bash
175+RT_LIB=/tmp/opencode/preload_test/real_runtime5/cann/lib64
176+LD_PRELOAD=./libhijack_real.so LD_LIBRARY_PATH=$RT_LIB:$LD_LIBRARY_PATH ./test_real
177+```
178+ 
179+## 测试结果
180+ 
181+### 符号分布
182+ 
183+```
184+libacl_rt.so: 0000000000030458 V g_hook_aclrtGetSocName ← 弱定义(Data 段)
185+libruntime.so: w g_hook_aclrtGetSocName ← 弱引用
186+```
187+ 
188+### 不带 LD_PRELOAD
189+ 
190+```
191+=== 真实Runtime LD_PRELOAD劫持测试 ===
192+ 
193+1. dlsym 查找 aclrtGetSocNameImpl:
194+ dlsym result = 0xffff8f36e284
195+ 
196+2. dlsym 查找 g_hook_aclrtGetSocName:
197+ g_hook_aclrtGetSocName = 0xffff8fa70458
198+ originalFunc = 0xffff8f36e284
199+ currentFunc = 0xffff8f36e284
200+ -> originalFunc 与 dlsym 结果一致
201+ 
202+3. 直接调用 aclrtGetSocName()(走libacl_rt.so转发):
203+ 返回值: "Ascend910_9362"
204+ -> 未被劫持,返回真实值
205+ 
206+4. 直接调用 aclrtGetSocNameImpl()(通过dlsym):
207+ 返回值: "Ascend910_9362"
208+ -> dlsym 直接调用未被劫持
209+ 
210+=== 测试结束 ===
211+```
212+ 
213+### 带 LD_PRELOAD
214+ 
215+```
216+=== 真实Runtime LD_PRELOAD劫持测试 ===
217+ 
218+1. dlsym 查找 aclrtGetSocNameImpl:
219+ dlsym result = 0xffff899305e8
220+ 
221+2. dlsym 查找 g_hook_aclrtGetSocName:
222+ g_hook_aclrtGetSocName = 0xffff89920458
223+ originalFunc = 0xffff899305e8
224+ currentFunc = 0xffff899305e8
225+ -> originalFunc 与 dlsym 结果一致
226+ 
227+3. 直接调用 aclrtGetSocName()(走libacl_rt.so转发):
228+ [PRELOAD] aclrtGetSocNameImpl hijacked!
229+ 返回值: "Ascend-HIJACKED-BY-PRELOAD"
230+ -> LD_PRELOAD 劫持成功! dlsym方式生效!
231+ 
232+4. 直接调用 aclrtGetSocNameImpl()(通过dlsym):
233+ [PRELOAD] aclrtGetSocNameImpl hijacked!
234+ 返回值: "Ascend-HIJACKED-BY-PRELOAD"
235+ -> dlsym 直接调用也被劫持!
236+ 
237+=== 测试结束 ===
238+```
239+ 
240+### 结果汇总
241+ 
242+| 场景 | `g_hook` 定义位置 | `originalFunc` 指向 | `aclrtGetSocName()` 返回值 | 劫持是否生效 |
243+|------|-------------------|---------------------|---------------------------|--------------|
244+| 不带 LD_PRELOAD | `libacl_rt.so` | `libruntime.so` 的真实实现 | `"Ascend910_9362"` | N/A |
245+| 带 LD_PRELOAD | `libacl_rt.so` | `libhijack_real.so` 的劫持实现 | `"Ascend-HIJACKED-BY-PRELOAD"` | **是** |
246+ 
247+### `dlopen("libruntime.so")` 独立加载测试
248+ 
249+验证 `libruntime.so` 不依赖 `libacl_rt.so` 也能独立加载(弱符号场景):
250+ 
251+```c
252+void *h = dlopen("libruntime.so", RTLD_NOW);
253+rtError_t (*fn)(char *, const uint32_t) = dlsym(h, "rtGetSocVersion");
254+char ver[256] = {0};
255+fn(ver, sizeof(ver));
256+```
257+ 
258+结果:
259+ 
260+```
261+rtGetSocVersion() ret=0 ver="Ascend910_9362"
262+```
263+ 
264+`libruntime.so` 独立加载成功,`rtGetSocVersion` 正常返回。
265+ 
266+### 多 API 劫持测试
267+ 
268+同时劫持 `aclrtGetSocNameImpl` 和 `aclrtGetVersionImpl`:
269+ 
270+| API | 不带 LD_PRELOAD | 带 LD_PRELOAD |
271+|-----|----------------|--------------|
272+| `aclrtGetSocName()` | `"Ascend910_9362"` | `"Ascend-HIJACKED-BY-PRELOAD"` |
273+| `aclrtGetVersion()` | `1.17.0` | `999.999.999` |
274+ 
275+不劫持时返回真实值,劫持时两个 `*Impl` 均被替换。
276+ 
277+## 结论
278+ 
279+`g_hook_*` 定义移至 `libacl_rt.so` 后,`LD_PRELOAD` 劫持 `*Impl` 函数成功生效。通过弱符号处理,`libruntime.so` 独立加载也正常工作。测试覆盖单 API 劫持、多 API 劫持、独立加载三个场景,均在真实 Runtime 包上验证通过。