LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

基于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

 

posted on 2024-03-29 23:59  ArnoldLu  阅读(753)  评论(0)    收藏  举报

导航