DrumatoのBlog

CS/Network/Infraが好きな桃です.

バイナリ何もわからない人に送る"ソースコード視点"のELFヘッダ解説

目次

注意

readelf 実装 で検索すると未だに一番上に出てきてしまっているので注意.
これはelfについて全然詳しくない時期に書いたものです.
一応入門的内容についてまとめたものがあるので,
よろしければそちらを.

zenn.dev

概要

ELFフォーマットを理解するための試み第二弾。
こちらの記事の続きと言ってもいいです。

drumato.hatenablog.com

上記の記事には間違いが多いので注意してください。
本記事バイナリ何もわからない人に送るELFヘッダ解説が正確です。

自作readelfの完成度を高める為にも、
単純にELFを勉強する教材として優れているという事からも、
本家readelfのコードリーディングは適切だと考えました。

今回は-hオプションに限定していきます。

まずはおさらい

まずはELF64ヘッダの構造体を見ながら、
ELFヘッダがどのように形成されているのか復習しましょう。

github.com

typedef struct elf64_hdr {
  unsigned char   e_ident[EI_NIDENT]; /* ELFのマジックナンバー*/
  Elf64_Half e_type;                  /* ELFバイナリの種類*/
  Elf64_Half e_machine;               /*アーキテクチャを識別する番号*/
  Elf64_Word e_version;               /*現在は基本的に1*/
  Elf64_Addr e_entry;                 /* 仮想アドレスのエントリポイント */
  Elf64_Off e_phoff;                  /* プログラムヘッダテーブルの開始位置 */
  Elf64_Off e_shoff;                  /* セクションヘッダテーブルの開始位置 */
  Elf64_Word e_flags; 
  Elf64_Half e_ehsize;                /*ELFヘッダのサイズ*/
  Elf64_Half e_phentsize;             /*プログラムヘッダのサイズ*/
  Elf64_Half e_phnum;                 /*プログラムヘッダの数*/
  Elf64_Half e_shentsize;             /*セクションヘッダのサイズ*/
  Elf64_Half e_shnum;                 /*セクションヘッダの数*/
  Elf64_Half e_shstrndx;              /*セクション名を指すインデックス*/
} Elf64_Ehdr;

Elf64_Half,Elf64_Word,Elf64_Addr,Elf64_Offはそれぞれ、

/* 64-bit ELF base types. */
typedef __u64  Elf64_Addr;  //unsigned-int64
typedef __u16  Elf64_Half;  //unsigned-int16
typedef __s16  Elf64_SHalf; //signed-int16
typedef __u64  Elf64_Off;   //unsigned-int64
typedef __s32  Elf64_Sword; //signed-int32
typedef __u32  Elf64_Word;  //unsigned-int32
typedef __u64  Elf64_Xword; //unsigned-int64
typedef __s64 Elf64_Sxword; //signed-int64

elf.hに記述されています。

C言語が分かりづらいなあという方の為に、
私の作ったreadelfも載せておきます。
(私のブログ見ている人には Go使い が多そうなので。)

type ElfHeader struct {
    MagicNumber         uint64 //マジックナンバー
    Class               uint16 //ELFバイナリの種類
    Data                uint16 //整数表現の種類
    Version             uint32 //基本1
    OSABI               uint16 //ex: UNIX - GNU
    ABIVersion          uint16 //使われてない
    Padding             uint32 //0パディング
    FileType            uint16 //実行ファイルか?共有オブジェクトファイルか?
    MachineArchitecture uint16 //アーキテクチャの種類を識別
    FileVersion         uint16 
    EntryPoint          uint64
    ProgramHeader       uint64
    SectionHeader       uint64
    Flags               uint32 //プロセッサ依存のフラグ。使われてない
    HeaderSize          uint16
    ProgramHeaderSize   uint16
    ProgramHeaderNum    uint16
    SectionHeaderSize   uint16
    SectionHeaderNum    uint16
    SectionNumber       uint16
}

ELFヘッダの構成については掴めたと思います。
図等でわかりやすく説明している記事に、

qiita.com

sugawarayusuke.hatenablog.com

http://softwaretechnique.jp/OS_Development/Tips/ELF/elf01.htmlsoftwaretechnique.jp

があるので参考にしてください。

ただソースコードが一番分かりやすいのかな?とは思います。


本題:readelf

重要なのはここまでで、
ここから先はコードを追っていくだけの単純作業です。

ここまでの事がわからなければこの先理解するのは難しいので、
読み直して理解してみてください。

まずは

github.com

から、-hオプションの解析部分を探します。

20k行近くある巨大なソースコードなので、
grepを駆使するように。

static struct option options[] =
{
  {"all",           no_argument, 0, 'a'},
  {"file-header",      no_argument, 0, 'h'},
  {"program-headers",  no_argument, 0, 'l'},
  {"headers",           no_argument, 0, 'e'},
  {"histogram",         no_argument, 0, 'I'},
  {"segments",          no_argument, 0, 'l'},
  {"sections",          no_argument, 0, 'S'},
  {"section-headers",  no_argument, 0, 'S'},
  {"section-groups",   no_argument, 0, 'g'},
  {"section-details",  no_argument, 0, 't'},
  {"full-section-name",no_argument, 0, 'N'},
  {"symbols",           no_argument, 0, 's'},
  {"syms",          no_argument, 0, 's'},
  {"dyn-syms",          no_argument, 0, OPTION_DYN_SYMS},
  {"relocs",        no_argument, 0, 'r'},
  {"notes",         no_argument, 0, 'n'},
  {"dynamic",           no_argument, 0, 'd'},
  {"arch-specific",    no_argument, 0, 'A'},
  {"version-info",     no_argument, 0, 'V'},
  {"use-dynamic",      no_argument, 0, 'D'},
  {"unwind",        no_argument, 0, 'u'},
  {"archive-index",    no_argument, 0, 'c'},
  {"hex-dump",          required_argument, 0, 'x'},
  {"relocated-dump",   required_argument, 0, 'R'},
  {"string-dump",      required_argument, 0, 'p'},
  {"decompress",       no_argument, 0, 'z'},
#ifdef SUPPORT_DISASSEMBLY
  {"instruction-dump", required_argument, 0, 'i'},
#endif
  {"debug-dump",       optional_argument, 0, OPTION_DEBUG_DUMP},

  {"dwarf-depth",      required_argument, 0, OPTION_DWARF_DEPTH},
  {"dwarf-start",      required_argument, 0, OPTION_DWARF_START},
  {"dwarf-check",      no_argument, 0, OPTION_DWARF_CHECK},

  {"version",           no_argument, 0, 'v'},
  {"wide",          no_argument, 0, 'W'},
  {"help",          no_argument, 0, 'H'},
  {0,             no_argument, 0, 0}
};

オプションの定義部分を見つけました。
このうちfile-headerのフローを読んでいきます。

引数解析部分はとても長いですが、
やっていることはとてもシンプル。

  • オプションを解析して、対応するフラグを立てる
  • 何も指定されていなければUsageを標準エラー出力
static void
parse_args (Filedata * filedata, int argc, char ** argv)
{
  int c;

  if (argc < 2)
    usage (stderr);

  while ((c = getopt_long
      (argc, argv, "ADHINR:SVWacdeghi:lnp:rstuvw::x:z", options, NULL)) != EOF)
    {
      switch (c)
    {
    case 0:
      /* Long options.  */
      break;
    case 'H':
      usage (stdout);
      break;

    case 'a':
      do_syms = TRUE;
      do_reloc = TRUE;
      do_unwind = TRUE;
      do_dynamic = TRUE;
      do_header = TRUE;
      do_sections = TRUE;
      do_section_groups = TRUE;
      do_segments = TRUE;
      do_version = TRUE;
      do_histogram = TRUE;
      do_arch = TRUE;
      do_notes = TRUE;
      break;
    case 'g':
      do_section_groups = TRUE;
      break;
    case 't':
    case 'N':
      do_sections = TRUE;
      do_section_details = TRUE;
      break;
    case 'e':
      do_header = TRUE;
      do_sections = TRUE;
      do_segments = TRUE;
      break;
    case 'A':
      do_arch = TRUE;
      break;
    case 'D':
      do_using_dynamic = TRUE;
      break;
    case 'r':
      do_reloc = TRUE;
      break;
    case 'u':
      do_unwind = TRUE;
      break;
    case 'h':
      do_header = TRUE;
      break;
    case 'l':
      do_segments = TRUE;
      break;
    case 's':
      do_syms = TRUE;
      break;
    case 'S':
      do_sections = TRUE;
      break;
    case 'd':
      do_dynamic = TRUE;
      break;
    case 'I':
      do_histogram = TRUE;
      break;
    case 'n':
      do_notes = TRUE;
      break;
    case 'c':
      do_archive_index = TRUE;
      break;
    case 'x':
      request_dump (filedata, HEX_DUMP);
      break;
    case 'p':
      request_dump (filedata, STRING_DUMP);
      break;
    case 'R':
      request_dump (filedata, RELOC_DUMP);
      break;
    case 'z':
      decompress_dumps = TRUE;
      break;
    case 'w':
      do_dump = TRUE;
      if (optarg == 0)
        {
          do_debugging = TRUE;
          dwarf_select_sections_all ();
        }
      else
        {
          do_debugging = FALSE;
          dwarf_select_sections_by_letters (optarg);
        }
      break;
    case OPTION_DEBUG_DUMP:
      do_dump = TRUE;
      if (optarg == 0)
        do_debugging = TRUE;
      else
        {
          do_debugging = FALSE;
          dwarf_select_sections_by_names (optarg);
        }
      break;
    case OPTION_DWARF_DEPTH:
      {
        char *cp;

        dwarf_cutoff_level = strtoul (optarg, & cp, 0);
      }
      break;
    case OPTION_DWARF_START:
      {
        char *cp;

        dwarf_start_die = strtoul (optarg, & cp, 0);
      }
      break;
    case OPTION_DWARF_CHECK:
      dwarf_check = TRUE;
      break;
    case OPTION_DYN_SYMS:
      do_dyn_syms = TRUE;
      break;
#ifdef SUPPORT_DISASSEMBLY
    case 'i':
      request_dump (filedata, DISASS_DUMP);
      break;
#endif
    case 'v':
      print_version (program_name);
      break;
    case 'V':
      do_version = TRUE;
      break;
    case 'W':
      do_wide = TRUE;
      break;
    default:
      /* xgettext:c-format */
      error (_("Invalid option '-%c'\n"), c);
      /* Fall through.  */
    case '?':
      usage (stderr);
    }
    }

  if (!do_dynamic && !do_syms && !do_reloc && !do_unwind && !do_sections
      && !do_segments && !do_header && !do_dump && !do_version
      && !do_histogram && !do_debugging && !do_arch && !do_notes
      && !do_section_groups && !do_archive_index
      && !do_dyn_syms)
    usage (stderr);
}
case 'h':
  do_header = TRUE;

とあるように、
do_headerフラグがヘッダ解析を行うトリガーとなっているようです。

更にgrepを掛けていくと、

process_file_headerdo_headerの有無によって標準出力の内容を変えている事がわかりました。

その関数を見てみます。

コメントアウトを適宜入れています。

static bfd_boolean
process_file_header (Filedata * filedata)
{
  Elf_Internal_Ehdr * header = & filedata->file_header;

  if (   header->e_ident[EI_MAG0] != ELFMAG0 //マジックナンバー検査
      || header->e_ident[EI_MAG1] != ELFMAG1
      || header->e_ident[EI_MAG2] != ELFMAG2
      || header->e_ident[EI_MAG3] != ELFMAG3)
    {
      error
    (_("Not an ELF file - it has the wrong magic bytes at the start\n"));
      return FALSE;
    }

  init_dwarf_regnames (header->e_machine);

  if (do_header)
    {
      unsigned i;

      printf (_("ELF Header:\n"));
      printf (_("  Magic:   "));
      for (i = 0; i < EI_NIDENT; i++)
    printf ("%2.2x ", header->e_ident[i]); //16進2桁ずつ出力する
      printf ("\n");
      printf (_("  Class:                             %s\n"),
          get_elf_class (header->e_ident[EI_CLASS]));
      printf (_("  Data:                              %s\n"),
          get_data_encoding (header->e_ident[EI_DATA]));
      printf (_("  Version:                           %d%s\n"),
          header->e_ident[EI_VERSION],
          (header->e_ident[EI_VERSION] == EV_CURRENT
           ? _(" (current)")
           : (header->e_ident[EI_VERSION] != EV_NONE
          ? _(" <unknown>")
          : "")));
      printf (_("  OS/ABI:                            %s\n"),
          get_osabi_name (filedata, header->e_ident[EI_OSABI]));
      printf (_("  ABI Version:                       %d\n"),
          header->e_ident[EI_ABIVERSION]);
      printf (_("  Type:                              %s\n"),
          get_file_type (header->e_type));
      printf (_("  Machine:                           %s\n"),
          get_machine_name (header->e_machine));
      printf (_("  Version:                           0x%lx\n"),
          header->e_version);

      printf (_("  Entry point address:               "));
      print_vma (header->e_entry, PREFIX_HEX);
      printf (_("\n  Start of program headers:          "));
      print_vma (header->e_phoff, DEC);
      printf (_(" (bytes into file)\n  Start of section headers:          "));
      print_vma (header->e_shoff, DEC);
      printf (_(" (bytes into file)\n"));

      printf (_("  Flags:                             0x%lx%s\n"),
          header->e_flags,
          get_machine_flags (filedata, header->e_flags, header->e_machine));
      printf (_("  Size of this header:               %u (bytes)\n"),
          header->e_ehsize);
      printf (_("  Size of program headers:           %u (bytes)\n"),
          header->e_phentsize);
      printf (_("  Number of program headers:         %u"),
          header->e_phnum);
      if (filedata->section_headers != NULL
      && header->e_phnum == PN_XNUM
      && filedata->section_headers[0].sh_info != 0)
    {
      header->e_phnum = filedata->section_headers[0].sh_info;
      printf (" (%u)", header->e_phnum);
    }
      putc ('\n', stdout);
      printf (_("  Size of section headers:           %u (bytes)\n"),
          header->e_shentsize);
      printf (_("  Number of section headers:         %u"),
          header->e_shnum);
      if (filedata->section_headers != NULL && header->e_shnum == SHN_UNDEF)
    {
      header->e_shnum = filedata->section_headers[0].sh_size;
      printf (" (%u)", header->e_shnum);
    }
      putc ('\n', stdout);
      printf (_("  Section header string table index: %u"),
          header->e_shstrndx);
      if (filedata->section_headers != NULL
      && header->e_shstrndx == (SHN_XINDEX & 0xffff))
    {
      header->e_shstrndx = filedata->section_headers[0].sh_link;
      printf (" (%u)", header->e_shstrndx);
    }
      if (header->e_shstrndx != SHN_UNDEF
      && header->e_shstrndx >= header->e_shnum)
    {
      header->e_shstrndx = SHN_UNDEF;
      printf (_(" <corrupt: out of range>"));
    }
      putc ('\n', stdout);
    }

  if (filedata->section_headers != NULL)
    {
      if (header->e_phnum == PN_XNUM
      && filedata->section_headers[0].sh_info != 0)
    header->e_phnum = filedata->section_headers[0].sh_info;
      if (header->e_shnum == SHN_UNDEF)
    header->e_shnum = filedata->section_headers[0].sh_size;
      if (header->e_shstrndx == (SHN_XINDEX & 0xffff))
    header->e_shstrndx = filedata->section_headers[0].sh_link;
      if (header->e_shstrndx >= header->e_shnum)
    header->e_shstrndx = SHN_UNDEF;
      free (filedata->section_headers);
      filedata->section_headers = NULL;
    }

  return TRUE;
}

ELFのヘッダを定義する構造体のメンバに対して、
いろいろな関数を適用しながら出力しているのがわかります。

readelfの出力にバリエーションをつけたい場合、
これら定義を一つ一つ追って行けば良さそうです。


総評

ELFヘッダの完全理解を目指しました。
バイナリ生成に必要な知識の一つだと考えます。