基于ldd快速遍历应用/库的依赖关系
对于复杂的应用或库文件,要了解其运作原理、架构,通过了解其库依赖关系不失为一种手段。
ldd可以查看单个可执行文件或库文件以来的库,但是库比较多的话不容易有个全局概念。
所以基于ldd编解Python脚本,做个小工具,提供集中查看方式:
- 直接文本查看类似tree命令输出的属性结构。
- xml格式的树形结构。
- dot格式数据,然后转换成png查看调用关系。
第一种最简单,可以嵌入式设备直接查看;最后一种最直观,最有效。
1 ldd
ldd可以显示一个应用程序或者库文件以来的库文件,基于这个命令可以遍历所有被依赖的库文件。
重点看ldd结果输出的依赖库文件的路径,可以被ldd再次调用,递归遍历所有的库文件。
ldd /usr/bin/weston linux-vdso.so.1 (0x0000007f8f9e5000) libexec_weston.so.0 => /usr/lib/weston/libexec_weston.so.0 (0x0000007f8f960000) libc.so.6 => /lib64/libc.so.6 (0x0000007f8f7f0000) libweston-11.so.0 => /usr/lib64/libweston-11.so.0 (0x0000007f8f780000) libwayland-client.so.0 => /usr/lib64/libwayland-client.so.0 (0x0000007f8f760000) libwayland-server.so.0 => /usr/lib64/libwayland-server.so.0 (0x0000007f8f730000) libinput.so.10 => /usr/lib64/libinput.so.10 (0x0000007f8f6d0000) libevdev.so.2 => /usr/lib64/libevdev.so.2 (0x0000007f8f6a0000) /lib/ld-linux-aarch64.so.1 (0x0000007f8f9b2000) libpixman-1.so.0 => /usr/lib64/libpixman-1.so.0 (0x0000007f8f630000) libdrm.so.2 => /usr/lib64/libdrm.so.2 (0x0000007f8f600000) libxkbcommon.so.0 => /usr/lib64/libxkbcommon.so.0 (0x0000007f8f5a0000) libmali_hook.so.1 => /usr/lib64/libmali_hook.so.1 (0x0000007f8f580000) libmali.so.1 => /usr/lib64/libmali.so.1 (0x0000007f88b60000) libffi.so.8 => /usr/lib64/libffi.so.8 (0x0000007f88b40000) libmtdev.so.1 => /usr/lib64/libmtdev.so.1 (0x0000007f88b20000) libudev.so.1 => /lib64/libudev.so.1 (0x0000007f88ae0000) libm.so.6 => /lib64/libm.so.6 (0x0000007f88a50000) librga.so.2 => /usr/lib64/librga.so.2 (0x0000007f88a20000) libdl.so.2 => /lib64/libdl.so.2 (0x0000007f88a00000) libpthread.so.0 => /lib64/libpthread.so.0 (0x0000007f889e0000) libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000007f88850000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000007f88820000)
2 基于ldd编写python脚本
2.1 生成类似tree库调用关系
#!/usr/bin/python import sys, os def interate_elf_libraries(header, elf_name): result = os.popen("ldd %s"%(elf_name)) header = header + " " for library in result.readlines(): library_split= library.split() if len(library_split) == 4: elf_name = library_split[2] if( not ("libm.so" in elf_name or "libc.so" in elf_name or "libgcc_s.so" in elf_name or "libdl.so" in elf_name or "libpthread.so" in elf_name or "libstdc++.so" in elf_name)): print(header + "|-" + elf_name) interate_elf_libraries(header, elf_name) result.close() if __name__ == '__main__': if len(sys.argv) < 2: print("No input argument.") sys.exit(1) header = "" elf_name = sys.argv[1] print(header + elf_name) interate_elf_libraries(header, elf_name)
执行./ldd-ext.py /usr/bin/weston,结果如下:
/usr/bin/weston |-/usr/lib/weston/libexec_weston.so.0 |-/usr/lib64/libweston-11.so.0 |-/usr/lib64/libwayland-server.so.0 |-/usr/lib64/libffi.so.8 |-/usr/lib64/libpixman-1.so.0 |-/usr/lib64/librga.so.2 |-/usr/lib64/libdrm.so.2 |-/usr/lib64/libdrm.so.2 |-/usr/lib64/libdrm.so.2 |-/usr/lib64/libxkbcommon.so.0 |-/usr/lib64/libmali_hook.so.1 |-/usr/lib64/libmali.so.1 |-/usr/lib64/libdrm.so.2 |-/usr/lib64/libwayland-client.so.0 |-/usr/lib64/libffi.so.8 |-/usr/lib64/libwayland-server.so.0 |-/usr/lib64/libffi.so.8 |-/usr/lib64/libffi.so.8 ...
2.2 生成xml格式的库调用关系
#!/usr/bin/python import sys, os def interate_elf_libraries(header, elf_name): result = os.popen("ldd %s"%(elf_name)) result_lines = result.readlines() elf_has_sub=0 for library in result_lines: library_split= library.split() if len(library_split) == 4: elf_name = library_split[2] if( not ("libm.so" in elf_name or "libc.so" in elf_name or "libgcc_s.so" in elf_name or "libdl.so" in elf_name or "libpthread.so" in elf_name or "libstdc++.so" in elf_name)): elf_has_sub=1 if( elf_has_sub == 1): print(header + "<sub>") header_sub = header + "\t" for library in result_lines: library_split= library.split() if len(library_split) == 4: elf_name = library_split[2] if( not ("libm.so" in elf_name or "libc.so" in elf_name or "libgcc_s.so" in elf_name or "libdl.so" in elf_name or "libpthread.so" in elf_name or "libstdc++.so" in elf_name)): print(header_sub + "<name>" + elf_name + "</name>") interate_elf_libraries(header_sub, elf_name) print(header + "</sub>") result.close() if __name__ == '__main__': if len(sys.argv) < 2: print("No input argument.") sys.exit(1) print("<?xml version='1.0' encoding='UTF-8'?>") header = "" elf_name = sys.argv[1] print("<app>") print(header + "<name>" + elf_name + "</name>") interate_elf_libraries(header, elf_name) print("</app>")
执行./ldd-ext.py /usr/bin/weston > weston.xml,然后再浏览器中查看如下:

2.3 生成dot格式库调用关系,然后转成png图片
大概流程如下:
- ldd获取被调用的库文件路径。
- python递归调用ldd,并保存成dot格式的文件。
- 使用dot命令根据dot数据生成png等格式。
#!/usr/bin/python import sys, os, re #exclude_libs = ["libm.so", "libc.so", "libgcc_s.so", "libpthread.so", "libstdc++.so", "libdl.so", "libogg.so", "libffi.so", "librga.so", "libglib-2.0.so", "libz.so", "libgthread-2.0.so", "libpcre.so", "libpcre2-16.so"] exclude_libs = ["libm.so", "libc.so", "libgcc_s.so", "libpthread.so", "libstdc++.so", "libdl.so", "libz.so"] def interate_elf_libraries(header, elf_name): result = os.popen("ldd %s"%(elf_name)) regex_pattern = '|'.join(map(re.escape, exclude_libs)) header = header + " " for library in result.readlines(): library_split= library.split() if len(library_split) == 4:#这种情况才会包含库文件路径。 sub_elf_name = library_split[2] if not re.search(regex_pattern, sub_elf_name, re.IGNORECASE):#排除某些不重要,或者不想查看的库文件。 print("\"" + elf_name + "\"->\"" + sub_elf_name + "\";") interate_elf_libraries(header, sub_elf_name)#递归遍历被依赖的库文件。 result.close() if __name__ == '__main__': if len(sys.argv) < 2: print("No input argument.") sys.exit(1) header = "" elf_name = sys.argv[1] print("strict digraph lib_call {")#一定要用strict,起到去重作用;使用digraph表示需要方向。 interate_elf_libraries(header, elf_name) print("}")
执行./ldd-ext.py /usr/bin/weston > weston.dot,结果如下:
strict digraph lib_call { "/usr/bin/weston"->"/usr/lib/weston/libexec_weston.so.0"; "/usr/lib/weston/libexec_weston.so.0"->"/usr/lib64/libweston-11.so.0"; ..."/usr/bin/weston"->"/usr/lib64/librga.so.2"; "/usr/lib64/librga.so.2"->"/usr/lib64/libdrm.so.2"; }
执行命令dot -Tpng weston.dot -o weston.png,生成png图片:

关于Graphviz/dot更多参考《Documentation | Graphviz》。
2.4 修复not found问题
#!/usr/bin/env python3 """ Generate a dependency graph (Graphviz DOT) for an ELF binary using ldd. Usage: ./deps_graph.py <elf-path> > graph.dot dot -Tpng graph.dot -o graph.png """ import os import re import sys import subprocess from pathlib import Path from typing import Optional, Tuple, List, Set # Base names that should be ignored. Versioned variants (e.g. libz.so.1) will # be matched automatically. EXCLUDE_NAMES: Set[str] = { "libm.so", "libc.so", "libgcc_s.so", "libpthread.so", "libstdc++.so", "libdl.so", "libz.so", } def compile_exclude_patterns(names: Set[str]) -> List[re.Pattern]: """Convert base names into regex that also match versioned variants.""" patterns: List[re.Pattern] = [] for name in names: escaped = re.escape(name) if escaped.endswith(r"\.so"): patterns.append(re.compile(rf"^{escaped}(?:\.\d+)*$", re.IGNORECASE)) else: patterns.append(re.compile(rf"^{escaped}$", re.IGNORECASE)) return patterns EXCLUDE_PATTERNS = compile_exclude_patterns(EXCLUDE_NAMES) visited: Set[str] = set() def should_exclude(lib_path: Optional[str], dep_name: str) -> bool: """Return True if the dependency should be ignored (matches base or versioned name).""" candidates = [] if lib_path: candidates.append(os.path.basename(lib_path)) candidates.append(dep_name) for candidate in candidates: for pattern in EXCLUDE_PATTERNS: if pattern.match(candidate): return True return False def parse_ldd_line(line: str) -> Optional[Tuple[str, Optional[str], bool]]: """ Parse a single ldd output line. Returns: (dependency_name, resolved_path, missing_flag) or None if the line can't be interpreted. """ line = line.strip() if not line: return None if "=>" in line: left, right = [part.strip() for part in line.split("=>", 1)] if right == "not found": return left, None, True parts = right.split() if not parts: return left, None, True return left, parts[0], False tokens = line.split() if tokens and tokens[0].endswith(".so"): return tokens[0], tokens[0], False return None def iterate_elf_libraries(elf_name: str) -> None: """Recursively walk the dependency graph for the given ELF.""" real_path = os.path.realpath(elf_name) if real_path in visited: return visited.add(real_path) try: result = subprocess.run( ["ldd", elf_name], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) lines = result.stdout.splitlines() except subprocess.CalledProcessError as exc: sys.stderr.write(f"[warn] ldd {elf_name} failed: {exc.stderr}\n") return except FileNotFoundError: sys.stderr.write(f"[warn] cannot run ldd on {elf_name}\n") return for line in lines: parsed = parse_ldd_line(line) if not parsed: continue dep_name, dep_path, missing = parsed if missing: target_label = f"{dep_name} (not found)" print(f' "{elf_name}" -> "{target_label}";') continue if should_exclude(dep_path, dep_name): continue print(f' "{elf_name}" -> "{dep_path}";') iterate_elf_libraries(dep_path) def main() -> None: if len(sys.argv) < 2: print("Usage: deps_graph.py <elf-path>") sys.exit(1) elf = sys.argv[1] if not Path(elf).exists(): print(f"error: '{elf}' does not exist", file=sys.stderr) sys.exit(1) print("strict digraph lib_call {") iterate_elf_libraries(elf) print("}") if __name__ == "__main__": main()
使用:
./deps_graph.py $(which rocgdb) > deps.dot
dot -Tpng deps.dot -o deps.png
联系方式:arnoldlu@qq.com
浙公网安备 33010602011771号