服务器之家:专注于服务器技术及软件下载分享
分类导航

Linux|Centos|Ubuntu|系统进程|Fedora|注册表|Bios|Solaris|Windows7|Windows10|Windows11|windows server|

服务器之家 - 服务器系统 - Linux - Linux从源码分析ldconfig命令对可执行文件缓存信息的读取原理(缓存文件的读)

Linux从源码分析ldconfig命令对可执行文件缓存信息的读取原理(缓存文件的读)

2023-12-25 17:03未知服务器之家 Linux

今日问题:Linux的ldconfig -p命令可打印出系统缓存已记录的所有动态库的信息。那么这个功能是如何实现的? 本文主要通过解读Linux的ldconfig命令的关键代码,分析了ldconfig命令是如何实现读取缓存文件 /etc/ld.so.cache 的内容的。本文

今日问题:Linux的ldconfig -p命令可打印出系统缓存已记录的所有动态库的信息。那么这个功能是如何实现的?

本文主要通过解读Linux的ldconfig命令的关键代码,分析了ldconfig命令是如何实现读取缓存文件 /etc/ld.so.cache 的内容的。本文涉及到的ldconfig的cache.c 代码文件网址[1],在参考资料里。

Linux从源码分析ldconfig命令对可执行文件缓存信息的读取原理(缓存文件的读)

ldconfig 使用的 /etc/ld.so.cache 文件,曾出现过两个版本:

1.老版本的缓存文件格式 老版本指libc5 格式的动态库,在glibc 2.0/2.1版本时采用的格式。缓存文件内容由cache_file类型的数据结构填充,其定义为

struct cache_file
{
  char magic[sizeof CACHEMAGIC - 1];
  unsigned int nlibs; /* 记录的条数*/
  struct file_entry libs[0];
};

2.新版本的的缓存文件格式 新版本指glibc 2.2及之后版本的。缓存文件内容由cache_file_new数据结构填充。定义为:

struct cache_file_new
{
  char magic[sizeof CACHEMAGIC_NEW - 1];
  char version[sizeof CACHE_VERSION - 1];
  uint32_t nlibs;  /* 记录的条数 */
  uint32_t len_strings;  /* Size of string table. */

  /* flags & cache_file_new_flags_endian_mask is one of the values
     cache_file_new_flags_endian_unset, cache_file_new_flags_endian_invalid,
     cache_file_new_flags_endian_little, cache_file_new_flags_endian_big.

     The remaining bits are unused and should be generated as zero and
     ignored by readers.  */
  uint8_t flags;

  uint8_t padding_unsed[3]; /* Not used, for future extensions.  */

  /* File offset of the extension directory.  See struct
     cache_extension below.  Must be a multiple of four.  */
  uint32_t extension_offset;

  uint32_t unused[3];  /* Leave space for future extensions
       and align to 8 byte boundary.  */
  struct file_entry_new libs[0]; /* Entries describing libraries.  */
  /* After this the string table of size len_strings is found. */
};

glibc-ld.so.cache1.1��� 以上输出信息确实以glibc-ld.so.cache开始,所以我用的Ubuntu22.04系统的ldconfig的缓存文件内容是新格式的。

ldconfig代码的cache.c 文件里是这样根据magic的不同用if(){} else{}处理的:

if (memcmp (cache->magic, CACHEMAGIC, sizeof CACHEMAGIC - 1)) {///当属于老版本时,按这里的方式处理 /* This can only be the new format without the old one. */ cache_new = (struct cache_file_new *) cache;

if (memcmp (cache_new->magic, CACHEMAGIC_NEW, sizeof CACHEMAGIC_NEW - 1)

在glibc-2.35的代码中已用英文说明了,glibc2.2格式的,能兼容glibc2.2之前的缓存文件内容。这里说的兼容,是依赖于代码检测实现的:由于两种结构体都以magic作为第一个项目,来识别缓存文件类型。再根据magic值的不同,对后续数据段采用不同的处理方式。老magic的定义为#define CACHEMAGIC "ld.so-1.7.0",新magic的定义为#define CACHEMAGIC_NEW "glibc-ld.so.cache"。也就是老版本 cache_file 的文件头部以字符串ld.so-1.7.0开始,新版本cache_file_new 的文件头部以字符串glibc-ld.so.cache开始。这点我们可以用head -c 命令查看下/etc/ld.so.cache文件的头部30个字符串旧可以验证了:

# head -c 30  /etc/ld.so.cache
glibc-ld.so.cache1.1���

以上输出信息确实以glibc-ld.so.cache开始,所以我用的Ubuntu22.04系统的ldconfig的缓存文件内容是新格式的。

ldconfig代码的cache.c 文件里是这样根据magic的不同用if(){} else{}处理的:

if (memcmp (cache->magic, CACHEMAGIC, sizeof CACHEMAGIC - 1))
    {///当属于老版本时,按这里的方式处理
      /* This can only be the new format without the old one.  */
      cache_new = (struct cache_file_new *) cache;

      if (memcmp (cache_new->magic, CACHEMAGIC_NEW, sizeof CACHEMAGIC_NEW - 1)
   || memcmp (cache_new->version, CACHE_VERSION,
        sizeof CACHE_VERSION - 1))
 error (EXIT_FAILURE, 0, _("File is not a cache file.\n"));
      check_new_cache (cache_new);
      format = 1;
      /* This is where the strings start.  */
      cache_data = (const char *) cache_new;
    }
  else
    {//当属于新版本缓存文件的时候,按下面内容处理
      ……省略
    }

在知道了 缓存文件类型(magic标记)后,就可以开始根据格式标准,逐条读/写每条记录了,这是ldconfig的重头戏。

先看对cache文件的读取效果,以 ldconfig -p命令打印出缓存文件的所有记录的结果为例:

# ldconfig -p
1525 libs found in cache `/etc/ld.so.cache
……
  libGLESv1_CM.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libGLESv1_CM.so
  libGL.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libGL.so.1
  libGL.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libGL.so
……

这里每条都是一个动态库的名称、格式(libc6等格式)、CPU架构、所在路径的记录。

缓存文件中的这么一条记录,对应的结构体,旧版本的为file_entry,新版本的为file_entry_new。它们的定义分别为:

struct file_entry
{
  int32_t flags;  /* This is 1 for an ELF library.  */
  uint32_t key, value;  /* String table indices.  */
};

以及新版本的 file_entry格式:

struct file_entry_new ///文件记录的新格式,增加了OS版本、硬件信息
{
  union
  {
    /* Fields shared with struct file_entry.  */
    struct file_entry entry;
    /* Also expose these fields directly.  */
    struct
    {
      int32_t flags;  /* This is 1 for an ELF library.  */
      uint32_t key, value; /* String table indices.  */
    };
  };
  uint32_t osversion;  /* Required OS version.  */
  uint64_t hwcap;  /* Hwcap entry.  */
};

继续分析【读缓存文件】的简要流程:

使用了 mmap() 函数,将 /etc/ld.so.cache 缓存文件整体读入内存:

struct cache_file *cache
    = mmap (NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

这是通过mmap()函数,将打开的缓存文件(open(/etc/ld.so.cache)的句柄fd)的数据映射到内存,由于文件数据就是按struct cache_file_new结构体格式填充的,所以mmap()后,就可以按这个结构体去解析各个条目。2. 判断magic,新老magic分流处理。3. 如果是新的magic,则按struct cache_file_new数据结构解析。4. 对于新格式,遍历读取数据、打印:

……
else{
      struct cache_extension_all_loaded ext;
      if (……)错误处理;

      /* Print everything.  */
      for (unsigned int i = 0; i < cache_new->nlibs; i++)
 {
   const char *hwcaps_string
     = glibc_hwcaps_string (&ext, cache, cache_size,
       &cache_new->libs[i]);
    
   print_entry (cache_data + cache_new->libs[i].key,
         cache_new->libs[i].flags,
         cache_new->libs[i].osversion,
         cache_new->libs[i].hwcap, hwcaps_string,
         cache_data + cache_new->libs[i].value);
 }
      print_extensions (&ext);
}

这里关键内容是:

  • cache_data,代表了mmap()读取到的缓存文件内容;以cache_data的地址为初始地址,按偏移量cache_new->libs[i].key 相加后,可得到每条file_entry_new的入口,然后分别打印出记录内容,就实现了 ldconfig -p 的代码功能。
  • 动态库的条数,等于 cache_new->nlibs 这个变量的值。作为for循环遍历时的条件。
  • cache_new->libs[i].key 这里的key,在struct file_entry_new中的定义是:
uint32_t key, value;  /* String table indices.  */

key相当于第i条动态库记录的目录索引。通过索引可以查到value。在实现时,key和value都是数字,这个数字代表字符串相对于cache_data这个首地址的字节偏移量,例如key->value 即 cache_new->libs[i].key, cache_new->libs[i].value 43256 -> 43234

Linux从源码分析ldconfig命令对可执行文件缓存信息的读取原理(缓存文件的读)

总之,通过对结构体的合理使用,将缓存文件内容解析后,可打印出缓存文件中记录的所有已知动态库文件的信息。

void print_cache (const char *cache_name) 的函数代码结束之前,还做了一下内存回收工作:

  /* Cleanup.  */
  munmap (cache, cache_size);
  close (fd);

首先使用munmap()函数,将之前已映射内存数据做一下清除;然后关闭打开的cache缓存文件描述符。

本文主要通过解读Linux的ldconfig命令的关键代码,分析了ldconfig命令是如何实现读取缓存文件 /etc/ld.so.cache 的内容的。本文涉及到的ldconfig的cache.c 代码文件网址[1],在参考资料里。

参考资料

[1]ldconfig的cache.c 代码文件网址: https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/cache.c;h=8149f889bab9f9cb32a50e349991ba821e4db0dd;hb=HEAD

延伸 · 阅读

精彩推荐