为何加载自制共享库后ctypes可访问标准C库函数?如何限制及解析符号?
编辑说明:此前问题诊断完全错误,实际与链接相关而非ctypes本身(gcc会隐式链接libc),已找到解决方案,故标记为重复。
问题
- 使用ctypes加载自制共享库时,为何能访问标准C库函数?原本预期执行以下最小可复现示例时会触发AttributeError。
- 如何限制该行为(若可行)?
- [附加问题]:当尝试访问函数且自制库重定义了标准C库函数时,函数名称如何解析?
背景与动机
根据ctypes文档,CDLL应返回已加载共享库的表示,但实际行为不符。在实现标准C库函数时发现该问题,测试时修改代码不生效,实际调用的是系统函数。虽可为自制函数添加前缀作为变通方案,但希望理解为何ctypes能访问未显式引用的函数,且文档未提及该行为。
最小可复现示例(MRE)
test.py
#!/usr/bin/env python3 import ctypes import os my_lib = ctypes.CDLL(os.path.abspath("libfoo.so")) # 预期可正常执行(实际确实如此) print(f"{my_lib.foo = }") # 预期执行失败(实际未失败) print(f"{my_lib.strncmp = }") # 预期执行失败(实际确实失败) print(f"{my_lib.bar = }")
foo.s
global foo foo: ret
Shell会话
$ make nasm -f elf64 -o foo.o foo.s gcc -shared -o libfoo.so foo.o $ nm -a libfoo.so 0000000000000000 a w __cxa_finalize@GLIBC_2.2.5 0000000000004000 d __dso_handle 0000000000003e48 d _DYNAMIC 00000000000010f4 t _fini 00000000000010f0 T foo 0000000000000000 a foo.s 0000000000003fe8 d _GLOBAL_OFFSET_TABLE_ w __gmon_start__ 0000000000001000 t _init w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable 0000000000004008 d __TMC_END__ $ ./test.py my_lib.foo = <_FuncPtr object at 0x75a09cb3e140> my_lib.strncmp = <_FuncPtr object at 0x75a09cb3e200> Traceback (most recent call last): File "/home/vmonteco/code/MREs/MRE_ctypes_give_access_to_stdlib/./test.py", line 13, in <module> print(f"{my_lib.bar = }") File "/home/vmonteco/.pyenv/versions/3.10.14/lib/python3.10/ctypes/__init__.py", line 387, in __getattr__ func = self.__getitem__(name) File "/home/vmonteco/.pyenv/versions/3.10.14/lib/python3.10/ctypes/__init__.py", line 392, in __getitem__ func = self._FuncPtr((name_or_ordinal, self)) AttributeError: /home/vmonteco/code/MREs/MRE_ctypes_give_access_to_stdlib/libfoo.so: undefined symbol: bar $ ldd libfoo.so linux-vdso.so.1 (0x00007610a363b000) libc.so.6 => /usr/lib/libc.so.6 (0x00007610a341d000) /usr/lib64/ld-linux-x86-64.so.2 (0x00007610a363d000) $ readelf -ld libfoo.so Elf file type is DYN (Shared object file) Entry point 0x0 There are 8 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000428 0x0000000000000428 R 0x1000 LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000101 0x0000000000000101 R E 0x1000 LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000004 0x0000000000000004 R 0x1000 LOAD 0x0000000000002e38 0x0000000000003e38 0x0000000000003e38 0x00000000000001d0 0x00000000000001d8 RW 0x1000 DYNAMIC 0x0000000000002e48 0x0000000000003e48 0x0000000000003e48 0x0000000000000180 0x0000000000000180 RW 0x8 NOTE 0x0000000000000200 0x0000000000000200 0x0000000000000200 0x0000000000000024 0x0000000000000024 R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000002e38 0x0000000000003e38 0x0000000000003e38 0x00000000000001c8 0x00000000000001c8 R 0x1 Section to Segment mapping: Segment Sections... 00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn 01 .init .text .fini 02 .eh_frame 03 .init_array .fini_array .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.gnu.build-id 06 07 .init_array .fini_array .dynamic .got .got.plt Dynamic section at offset 0x2e48 contains 20 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] 0x000000000000000c (INIT) 0x1000 0x000000000000000d (FINI) 0x10f4 0x0000000000000019 (INIT_ARRAY) 0x3e38 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) 0x000000000000001a (FINI_ARRAY) 0x3e40 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x228 0x0000000000000005 (STRTAB) 0x2e0 0x0000000000000006 (SYMTAB) 0x250 0x000000000000000a (STRSZ) 111 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000007 (RELA) 0x380 0x0000000000000008 (RELASZ) 168 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffffe (VERNEED) 0x360 0x000000006fffffff (VERNEEDNUM) 1 0x000000006ffffff0 (VERSYM) 0x350 0x000000006ffffff9 (RELACOUNT) 3 0x0000000000000000 (NULL) 0x0 $
补充信息:版本与系统信息
uname -a:Archlinux (Linux vmonteco-P15 6.10.10-arch1-1 #1 SMP PREEMPT_DYNAMIC Thu, 12 Sep 2024 17:21:02 +0000 x86_64 GNU/Linux)python:Python 3.10.14nasm:NASM version 2.16.03 compiled on May 4 2024gcc:gcc (GCC) 14.2.1 20240910ld:GNU ld (GNU Binutils) 2.43.0
解决方案与解答
1. 为何能访问标准C库函数?
这并非ctypes的问题,而是gcc默认会隐式链接libc。从ldd和readelf的输出可见,libfoo.so的动态依赖包含libc.so.6,当通过ctypes加载该共享库时,系统动态链接器会自动加载所有依赖库。ctypes查找符号时,实际是通过动态链接器在整个进程的全局符号表中检索,而非仅目标共享库的符号表——因此能找到libc中的strncmp,但找不到不存在的bar。
2. 如何限制该行为?
可通过修改链接选项,让自制库不隐式链接libc:
- 使用
gcc -shared -o libfoo.so foo.o -nostdlib编译,生成的库将不再依赖libc,此时用ctypes访问strncmp会触发AttributeError。注意:-nostdlib会移除所有标准库依赖,包括初始化/终止函数,若库需要这些功能需手动处理。 - 或使用
-nodefaultlibs,它会移除默认链接的库,但保留ld-linux.so等必要的链接器脚本。
3. 重定义标准库函数时的符号解析
若自制库中重定义了标准C库函数(如自行实现strncmp),动态链接器会优先使用自制库中的符号。因为动态链接器的符号查找顺序是:先查找已加载的库,加载共享库时其符号会加入全局符号表,后续查找同名符号时会优先匹配该库的实现,除非使用-fvisibility=hidden等选项限制符号可见性,或链接时用-Bsymbolic让库优先使用自身符号。
内容的提问来源于stack exchange,提问作者vmonteco
相关产品推荐
相关产品推荐

