[Linux][C/C++]通过ELF读取当前进程的符号表并获取符号的版本信息

Linux 采用 ELF 作为其可链接可执行文件的格式,并提供诸如 nm 之类的工具进行 ELF 符号表的解析。如下例程(vim test.cc):

#include <iostream>
#include <pthread.h>

int main()
{
  pthread_cond_t cond;
  pthread_condattr_t attr;

  pthread_condattr_init(&attr);
  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);

  int ret = pthread_cond_init(&cond, &attr);

  if (ret != 0) {
    std::cout << "call_pthread_cond_init failed." << std::endl;
    return ret;
  }

  pthread_cond_destroy(&cond);

  return 0;
}

我们进行编译,并通过 nm 查看可执行文件的符号表:

[Linux] $ g++ test.cc -lpthread
[Linux] $ nm -g a.out
0000000000400b88 R _IO_stdin_used
                 w _Jv_RegisterClasses
                 U _ZNSolsEPFRSoS_E@@GLIBCXX_3.4
                 U _ZNSt8ios_base4InitC1Ev@@GLIBCXX_3.4
                 U _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4
00000000006012a0 B _ZSt4cout@@GLIBCXX_3.4
                 U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@@GLIBCXX_3.4
0000000000601020 D __DTOR_END__
0000000000601284 A __bss_start
                 U __cxa_atexit@@GLIBC_2.2.5
0000000000601280 D __data_start
0000000000400b90 R __dso_handle
                 w __gmon_start__
                 U __gxx_personality_v0@@CXXABI_1.3
0000000000400aa0 T __libc_csu_fini
0000000000400ab0 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000601284 A _edata
00000000006013c8 A _end
0000000000400b78 T _fini
0000000000400808 T _init
00000000004008f0 T _start
0000000000601280 W data_start
00000000004009d4 T main
                 U pthread_cond_destroy@@GLIBC_2.3.2
                 U pthread_cond_init@@GLIBC_2.3.2
                 U pthread_condattr_init@@GLIBC_2.2.5
                 U pthread_condattr_setclock@@GLIBC_2.3.3

可以看到,glibc 库函数后面都有一个版本信息,这就又牵扯到了 symbol versioning 机制。1995年,Solaris 的 link editor 和 ld.so 引入了 symbol versioning 机制。知乎作者 MaskRay 的这篇文章对 GNU 的 symbol versioning 机制做了比较详细的描述,可以参考:All about symbol versioning - MaskRay的文章 - 知乎。此外,我们还可以从 ELF 的 man page 中找到对 symbol versioning entry 的描述。

我们以上面例程中引用的一个 glibc 函数为例,编译时,pthread_cond_init 这个函数使用的版本是GLIBC_2.3.2。而实际上,pthread 库中,pthread_cond_init 这个函数存在两个版本:

[Linux] $ nm -g /lib64/libpthread.so.0 | grep pthread_cond_init
000000390280b0b0 T pthread_cond_init@@GLIBC_2.3.2
000000390280c030 T pthread_cond_init@GLIBC_2.2.5

除了引用头文件 #include <pthread.h> 并显式调用库函数 pthread_cond_init 以外,glibc 还提供了 dlsym 函数,可以从 libpthread.so 中取得 pthread_cond_init 的指针:

#include <pthread.h>
#include <dlfcn.h>
#include <iostream>

typedef int (*cond_init_func_t)(pthread_cond_t *cond,
                                const pthread_condattr_t *attr);

extern "C" {

int pthread_cond_init(pthread_cond_t *cond,
                      const pthread_condattr_t *attr)
{
  return 0;
}

}

static int call_pthread_cond_init(pthread_cond_t *cond,
                                  pthread_condattr_t *attr)
{
  cond_init_func_t func =
      (cond_init_func_t) dlsym(RTLD_NEXT, "pthread_cond_init");
  return func(cond, attr);
}

int main()
{
  pthread_cond_t cond;
  pthread_condattr_t attr;

  pthread_condattr_init(&attr);
  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);

  int ret = call_pthread_cond_init(&cond, &attr);

  if (ret != 0) {
    std::cout << "call_pthread_cond_init failed." << std::endl;
    return ret;
  }

  pthread_cond_destroy(&cond);

  return 0;
}

如上,我们修改一下之前的例程,一方面,override 库函数 pthread_cond_init(glibc 库函数一般都声明为 weak symbol,用同名函数就可以 override);另一方面,使用 dlsym 获取 glibc 原生的 pthread_cond_init 函数指针,并调用它。

我们的预期是,call_pthread_cond_init 将会获得 pthread_cond_init@@GLIBC_2.3.2 的指针,并正常执行。但实际情况是怎样的呢?我们编译运行一下:

[Linux] $ g++ test.cc -lpthread -ldl
[Linux] $ ./a.out
call_pthread_cond_init failed.

很显然,这并不符合我们的预期。因为,实际上 dlsym 获取到的函数指针并不是 GLIBC_2.3.2 版本,而是 pthread_cond_init@GLIBC_2.2.5(2.2.5 版本的 pthread_cond_init 还不支持使用 CLOCK_MONOTONIC 类型的时钟)。也就是说,dlsym 并没有获取到这个 symbol 的默认版本,这是 glibc 的一个已知问题。

Glibc 自 2.1 版本开始,就引入了一个名为 dlvsym 的库函数,可以在获取函数指针时指定符号的版本。我们再修改一下上面的例程中的 call_pthread_cond_init 函数:

static int call_pthread_cond_init(pthread_cond_t *cond,
                                  pthread_condattr_t *attr)
{
  cond_init_func_t func =
      (cond_init_func_t) dlvsym(RTLD_NEXT,
                                "pthread_cond_init",
                                "GLIBC_2.3.2");
  return func(cond, attr);
}

编译运行一下:

[Linux] $ g++ test.cc -lpthread -ldl
[Linux] $ ./a.out

可以看到,获取到正确版本的函数指针以后,就不会报错了。

可是,我们如何同时做到既可以 override glibc 函数,又能够获取 glibc 函数的默认版本呢?可以通过动态库来实现。即,在动态库中 override glibc 函数,并利用 LD_PRELOAD 机制加载(类似于 jemalloc 库和 tcmalloc 库的做法);并在加载阶段从当前可执行程序的ELF中读取版本信息。我们把调用 pthread_cond_init 的例程(test.cc)恢复到初始的样子:

#include <iostream>
#include <pthread.h>

int main()
{
  pthread_cond_t cond;
  pthread_condattr_t attr;

  pthread_condattr_init(&attr);
  pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);

  int ret = pthread_cond_init(&cond, &attr);

  if (ret != 0) {
    std::cout << "call_pthread_cond_init failed." << std::endl;
    return ret;
  }

  pthread_cond_destroy(&cond);

  return 0;
}

然后,我们再实现一个动态库(readelf.cc),从 “/proc/self/exe” 中读取当前可执行文件的路径,并从该文件的ELF中读取符号表;同时,override pthread_cond_init 函数,通过之前获取的版本号,利用 dlvsym 获取 glibc 原生库函数的默认版本,并调用之:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include <link.h>
#include <string.h>
#include <pthread.h>

#include <string>
#include <iostream>
#include <algorithm>
#include <tr1/unordered_map>

typedef int (*cond_init_func_t)(pthread_cond_t *cond,
                                const pthread_condattr_t *attr);

using namespace std;

/*
  Symbol version map.
  Key:   symbol name.
  Value: version string.
*/
std::tr1::unordered_map<string, string> sym_versions;

static int call_pthread_cond_init(pthread_cond_t *cond,
                                  const pthread_condattr_t *attr)
{
  cond_init_func_t func =
      (cond_init_func_t) dlvsym(RTLD_NEXT,
                                "pthread_cond_init",
                                sym_versions[string("pthread_cond_init")].c_str());
  return func(cond, attr);
}

extern "C" {

int pthread_cond_init(pthread_cond_t *cond,
                      const pthread_condattr_t *attr)
{
  int ret = call_pthread_cond_init(cond, attr);
  cout << "Override pthread_cond_init (ret = " << ret << ")." << endl;
  return ret;
}

}

__attribute__((constructor))
void readelf()
{
  ElfW(Ehdr) ehdr;           // ELF header
  ElfW(Phdr*) phdrs = NULL;  // Program headers
  ElfW(Shdr*) shdrs = NULL;  // Section headers
  ElfW(Dyn*) dyns = NULL;    // Dynamic entrys

  ElfW(Sym*) syms = NULL;    // Symbol table
  ElfW(Word) sym_cnt = 0;    // Number of symbol entries

  char *strtab = NULL;       // String table
  ElfW(Word) strtab_sz = 0;  // Size in byte of string table
  ElfW(Off) strtab_off = 0;  // File offset of string table

  ElfW(Versym*) versyms = NULL;
  ElfW(Verneed*) verneeds = NULL;

  std::tr1::unordered_map<ElfW(Half), ElfW(Word)> vermap;

  char buffer[PATH_MAX];
  // Get the absolute path of current executable file.
  int res = readlink("/proc/self/exe", buffer, PATH_MAX);

  FILE *fp = fopen(buffer, "r");
  if (!fp) {
    cout << "Failed to open file: " << buffer << endl;
    return;
  }

  // Read the ELF header
  fread(&ehdr, 1, sizeof(ehdr), fp);

  // Check ELF magic numbers
  if (0 != strncmp((char *) ehdr.e_ident, ELFMAG, SELFMAG)) {
    cout << "Failed to check ELF magic numbers." << endl;
    goto out;
  }

  // Read the program headers
  phdrs = new ElfW(Phdr)[ehdr.e_phnum];
  fseek(fp, ehdr.e_phoff, SEEK_SET);
  fread(phdrs, ehdr.e_phnum, sizeof(ElfW(Phdr)), fp);

  cout << "Read " << ehdr.e_phnum << " program headers." << endl;

  for (int phdr_index = 0; phdr_index < ehdr.e_phnum; phdr_index++) {
    ElfW(Phdr*) phdr = &phdrs[phdr_index];

    if (phdr->p_type != PT_DYNAMIC)
      continue;

    cout << "Got the dynamic program header." << endl;

    dyns = (ElfW(Dyn*)) malloc(phdr->p_filesz);
    fseek(fp, phdr->p_offset, SEEK_SET);
    fread(dyns, phdr->p_filesz, sizeof(char), fp);
    for (ElfW(Dyn*) dyn = dyns; dyn->d_tag != DT_NULL; dyn++) {
      switch (dyn->d_tag) {
      case DT_STRSZ:
        strtab_sz = dyn->d_un.d_val;
        cout << "DT_STRSZ value: " << strtab_sz << "." << endl;
        break;
      default:
        break;
      }
    }

    break;
  }

  // Read section headers
  shdrs = new ElfW(Shdr)[ehdr.e_shnum];
  fseek(fp, ehdr.e_shoff, SEEK_SET);
  fread(shdrs, ehdr.e_shnum, sizeof(ElfW(Shdr)), fp);

  cout << "Read " << ehdr.e_shnum << " section headers." << endl;

  // Get the section name string table
  strtab = new char[std::max((ElfW(Word)) shdrs[ehdr.e_shstrndx].sh_size,
                             strtab_sz)];
  fseek(fp, shdrs[ehdr.e_shstrndx].sh_offset, SEEK_SET);
  fread(strtab, shdrs[ehdr.e_shstrndx].sh_size, sizeof(char), fp);

  // Read sections
  for (int s_idx = 0; s_idx < ehdr.e_shnum; s_idx++) {
    ElfW(Shdr*) sh = &shdrs[s_idx];
    //cout << s_idx << " " << strtab + sh->sh_name << endl;
    if (!strcmp(strtab + sh->sh_name, ".dynsym")) {
      sym_cnt = sh->sh_size / sizeof(ElfW(Sym));
      syms = new ElfW(Sym)[sym_cnt];
      fseek(fp, sh->sh_offset, SEEK_SET);
      fread(syms, sh->sh_size, sizeof(char), fp);
      cout << ".dynsym: got " << sym_cnt << " symbols." << endl;
    } else if (!strcmp(strtab + sh->sh_name, ".dynstr")) {
      cout << ".dynstr: offset " << sh->sh_offset
           << " size " << sh->sh_size << "." << endl;
      strtab_off = sh->sh_offset;
    } else if (!strcmp(strtab + sh->sh_name, ".gnu.version_r")) {
      cout << ".gnu.version_r: verneed offset " << sh->sh_offset
           << " size " << sh->sh_size
           << "." << endl;
      verneeds = (ElfW(Verneed*)) malloc(sh->sh_size);
      fseek(fp, sh->sh_offset, SEEK_SET);
      fread(verneeds, sh->sh_size, sizeof(char), fp);
    } else if (!strcmp(strtab + sh->sh_name, ".gnu.version")) {
      cout << ".gnu.version: versym offset " << sh->sh_offset
           << " size " << sh->sh_size
           << "." << endl;
      versyms = (ElfW(Versym*)) malloc(sh->sh_size);
      fseek(fp, sh->sh_offset, SEEK_SET);
      fread(versyms, sh->sh_size, sizeof(char), fp);
    }
  }

  // Get the symbol name string table
  fseek(fp, strtab_off, SEEK_SET);
  fread(strtab, strtab_sz, sizeof(char), fp);

  // Get verneeds
  for (ElfW(Verneed*) vn = verneeds; ; ) {
    cout << "verneed " << ":"
         << " vn_version " << vn->vn_version
         << " vn_cnt " << vn->vn_cnt
         << " vn_file " << strtab + vn->vn_file
         << " vn_aux " << vn->vn_aux
         << " vn_next " << vn->vn_next
         << "." << endl;

    ElfW(Vernaux*) vna = (ElfW(Vernaux*))((char*)vn + vn->vn_aux);
    for (ElfW(Half)i = 0; i < vn->vn_cnt; i++) {
      cout << "    aux " << i << ": "
           << " vna_name " << strtab + vna->vna_name
           << " vna_other " << vna->vna_other
           << "." << endl;

      vermap.insert(std::make_pair<ElfW(Half), ElfW(Word)>
                      (vna->vna_other, vna->vna_name));

      vna = (ElfW(Vernaux*))((char*)vna + vna->vna_next);
    }

    if (vn->vn_next == 0)
      break;

    vn = (ElfW(Verneed*)) ((char*)vn + vn->vn_next);
  }

  // Get versyms
  for (ElfW(Word) sym_index = 0; sym_index < sym_cnt; sym_index++) {
    ElfW(Sym*) sym = &syms[sym_index];
    const char *ver_name = "NONE";
    if (versyms[sym_index])
      ver_name = strtab + vermap[versyms[sym_index]];

    sym_versions.insert(std::make_pair<string, string>
                          (strtab + sym->st_name, ver_name));

    cout << "symbol " << strtab + sym->st_name
         << " version " << ver_name
         << "." << endl;
  }

out:
  fclose(fp);
  if (phdrs)
    delete [] phdrs;
  if (shdrs)
    delete [] shdrs;
  if (dyns)
    free(dyns);
  if (syms)
    delete [] syms;
  if (strtab)
    delete [] strtab;
  if (verneeds)
    free(verneeds);
  if (versyms)
    free(versyms);
}

我们分别编译,并以 LD_PRELOAD 方式运行上述程序:

[Linux] $ g++ test.cc -lpthread
[Linux] $ g++ -shared -fPIC -o readelf.so readelf.cc -ldl
[Linux] $ LD_PRELOAD=./readelf.so ./a.out
Read 8 program headers.
Got the dynamic program header.
DT_STRSZ value: 489.
Read 30 section headers.
.dynsym: got 16 symbols.
.dynstr: offset 1048 size 489.
.gnu.version: versym offset 1538 size 32.
.gnu.version_r: verneed offset 1576 size 144.
verneed : vn_version 1 vn_cnt 1 vn_file libc.so.6 vn_aux 16 vn_next 32.
    aux 0:  vna_name GLIBC_2.2.5 vna_other 4.
verneed : vn_version 1 vn_cnt 2 vn_file libstdc++.so.6 vn_aux 16 vn_next 48.
    aux 0:  vna_name CXXABI_1.3 vna_other 7.
    aux 1:  vna_name GLIBCXX_3.4 vna_other 3.
verneed : vn_version 1 vn_cnt 3 vn_file libpthread.so.0 vn_aux 16 vn_next 0.
    aux 0:  vna_name GLIBC_2.3.3 vna_other 6.
    aux 1:  vna_name GLIBC_2.2.5 vna_other 5.
    aux 2:  vna_name GLIBC_2.3.2 vna_other 2.
symbol  version NONE.
symbol pthread_cond_destroy version GLIBC_2.3.2.
symbol __gmon_start__ version NONE.
symbol _Jv_RegisterClasses version NONE.
symbol _ZNSt8ios_base4InitC1Ev version GLIBCXX_3.4.
symbol __libc_start_main version GLIBC_2.2.5.
symbol __cxa_atexit version GLIBC_2.2.5.
symbol _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc version GLIBCXX_3.4.
symbol pthread_cond_init version GLIBC_2.3.2.
symbol pthread_condattr_init version GLIBC_2.2.5.
symbol pthread_condattr_setclock version GLIBC_2.3.3.
symbol _ZNSolsEPFRSoS_E version GLIBCXX_3.4.
symbol _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ version GLIBCXX_3.4.
symbol _ZNSt8ios_base4InitD1Ev version GLIBCXX_3.4.
symbol _ZSt4cout version GLIBCXX_3.4.
symbol __gxx_personality_v0 version CXXABI_1.3.
Override pthread_cond_init (ret = 0).

如上,当前进程所依赖的符号版本都可以被正确获取,pthread_cond_init 使用了我们 hook 的版本,并且没有报错。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux ELF(可执行与可链接格式)中,有两个相关的符号表:静态符号表和动态符号表。它们与程序运行时的函数符号表有以下关系: 1. 静态符号表:静态符号表是在编译时由编译器创建的一张符号表,它包含了可执行文件中的所有符号信息,包括函数、全局变量、局部变量等。静态符号表记录了符号的名称、类型、地址等信息。它在链接过程中用于解析符号的引用关系,以确定符号的最终地址。 2. 动态符号表:动态符号表是在运行时由动态链接器(如 ld.so)创建的一张符号表,它包含了可执行文件中需要在运行时解析的动态符号信息,如共享库中的函数和变量。动态符号表记录了符号的名称、类型、地址等信息。它用于在程序运行时进行动态链接,将程序与共享库中的符号关联起来。 3. 函数符号表:函数符号表是在程序运行时动态生成的,用于记录程序当前可见的函数符号及其地址。这个表主要由动态链接器和调试器使用,用于调试目的或者在运行时查找函数地址。 静态符号表和动态符号表都记录了程序中的符号信息,但它们的使用方式和目的略有不同。静态符号表用于链接过程,解析符号引用关系以确定符号的地址。动态符号表用于动态链接过程,在程序运行时将符号与共享库中的实际地址关联起来。函数符号表则是在程序运行时动态生成的,用于调试和查找函数地址。 需要注意的是,函数符号表是一个在运行时动态生成和维护的结构,它的具体实现可能会因操作系统、编译器、调试器等因素而有所不同。以上是一般情况下的概述,具体实现细节可能会有所差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值