情弱ログ

参考にならないので当てにしないでください

ELF入門

C言語が書ける、アセンブラが吐き出すアセンブリコードが分かる、でもHello, worldするバイナリは何が書かれているか分からない…。というか、バイナリの実行って何?っていう疑問を解決したいバイナリ初心者のメモです。
今回はreadelfコマンドは知っていても、どんな意味がある出力なのかがいまいち分かっていなかったので、車輪の再開発で理解していこうという試みです。とりあえずreadelfに-h、-l、-Sオプションを指定した時と同じような出力ができるプログラムの作成を目指します。

まず、ELFファイルとはなんぞやという所からですが、Windowsでいうexeみたいなもんだと思います(乱暴)。ELFはExecutable and Linking Formatの略であり、例えばC言語で書かれたソースコードコンパイルして出来た実行可能ファイルはELFファイルです。
実行可能ファイルだけでなく、共有ライブラリや再配置可能オブジェクト、コアファイルもELFに従っています。これらの拡張子は.soファイルであったり.oファイルであったりします。コアファイルについては以前書いたこちらを参照してください。
1年生に送るデバッガ(gdb)の使い方(入門編) - 情弱ログ

このELFファイルですが、バイナリファイルであるため通常は内容を確認することはできません。逸般人の方はバイナリエディタを使って見ることができるらしいですが、通常はreadelfコマンドを用いて確認することになります。しかし、このreadelfコマンドは「分かっている人」向けの出力であり、初心者には少し出力が難解です。
そこで、まずはELFファイルの形式について勉強するため、簡単なELFファイルをダンプするプログラムを書いていきます。

コードの全容はこちら
github.com

何はともあれ、ELFファイルの構造が分かっていないとどうしようもないので、wikipediaあたりを見てみます。すると以下のような図があったので、以降はこの図を参考にしていきます。余談ですがman elfは滅茶苦茶丁寧に書かれているので一見の価値ありです。
https://upload.wikimedia.org/wikipedia/commons/a/ab/Elf-layout--en.png
wikipediaより

図から、ELFファイルの最初にELF header、続いてProgram header table、ファイルの最後にSection header tableがあることが分かります。次に、/usr/include以下にelf.hという、いかにもなヘッダファイルがあるので中身を確認していきます。

#include <stdint.h>

/* Type for a 16-bit quantity.  */
typedef uint16_t Elf64_Half;

/* Types for signed and unsigned 32-bit quantities.  */
typedef uint32_t Elf64_Word;
typedef	int32_t  Elf64_Sword;

/* Types for signed and unsigned 64-bit quantities.  */
typedef uint64_t Elf64_Xword;
typedef	int64_t  Elf64_Sxword;

/* Type of addresses.  */
typedef uint64_t Elf64_Addr;

/* Type of file offsets.  */
typedef uint64_t Elf64_Off;

/* Type for section indices, which are 16-bit quantities.  */
typedef uint16_t Elf64_Section;

/* Type for version symbol information.  */
typedef Elf64_Half Elf64_Versym;

/* The ELF file header.  This appears at the start of every ELF file.  */

#define EI_NIDENT (16)

typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf64_Half	e_type;			/* Object file type */
  Elf64_Half	e_machine;		/* Architecture */
  Elf64_Word	e_version;		/* Object file version */
  Elf64_Addr	e_entry;		/* Entry point virtual address */
  Elf64_Off	e_phoff;		/* Program header table file offset */
  Elf64_Off	e_shoff;		/* Section header table file offset */
  Elf64_Word	e_flags;		/* Processor-specific flags */
  Elf64_Half	e_ehsize;		/* ELF header size in bytes */
  Elf64_Half	e_phentsize;		/* Program header table entry size */
  Elf64_Half	e_phnum;		/* Program header table entry count */
  Elf64_Half	e_shentsize;		/* Section header table entry size */
  Elf64_Half	e_shnum;		/* Section header table entry count */
  Elf64_Half	e_shstrndx;		/* Section header string table index */
} Elf64_Ehdr;

/* Section header.  */

typedef struct
{
  Elf64_Word	sh_name;		/* Section name (string tbl index) */
  Elf64_Word	sh_type;		/* Section type */
  Elf64_Xword	sh_flags;		/* Section flags */
  Elf64_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf64_Off	sh_offset;		/* Section file offset */
  Elf64_Xword	sh_size;		/* Section size in bytes */
  Elf64_Word	sh_link;		/* Link to another section */
  Elf64_Word	sh_info;		/* Additional section information */
  Elf64_Xword	sh_addralign;		/* Section alignment */
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} Elf64_Shdr;

/* Program segment header.  */

typedef struct
{
  Elf64_Word	p_type;			/* Segment type */
  Elf64_Word	p_flags;		/* Segment flags */
  Elf64_Off	p_offset;		/* Segment file offset */
  Elf64_Addr	p_vaddr;		/* Segment virtual address */
  Elf64_Addr	p_paddr;		/* Segment physical address */
  Elf64_Xword	p_filesz;		/* Segment size in file */
  Elf64_Xword	p_memsz;		/* Segment size in memory */
  Elf64_Xword	p_align;		/* Segment alignment */
} Elf64_Phdr;

途中をかなり省略したり、64bit向けの構造体のみを抜き出したりしています。図のELF headerがElf64_Ehdrに、Program header tableがElf64_Phdrに、Section header tableがElf64_Shdrに対応しています。これらの情報を元に、まずELF (file) headerをダンプするプログラムを書いていきましょう。

#include <stdio.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <elf.h>

#define DUMP(x) do {printf("  " #x " = %u(0x%x)\n", (uint32_t)x, (uint32_t)x);} while(0);

void dump_ehdr(Elf64_Ehdr *ehdr){
    int i;
    printf("  ehdr->e_ident = ");
    for (i = 0; i < EI_NIDENT; i++) {
        printf("%02x ", ehdr->e_ident[i]);
    }
    printf("\n");
    DUMP(ehdr->e_type);
    DUMP(ehdr->e_machine);
    DUMP(ehdr->e_version);
    DUMP(ehdr->e_entry);
    DUMP(ehdr->e_phoff);
    DUMP(ehdr->e_shoff);
    DUMP(ehdr->e_flags);
    DUMP(ehdr->e_ehsize);
    DUMP(ehdr->e_phentsize);
    DUMP(ehdr->e_phnum);
    DUMP(ehdr->e_shentsize);
    DUMP(ehdr->e_shnum);
    DUMP(ehdr->e_shstrndx);
    printf("\n");
}

int main(void){
    Elf64_Ehdr *ehdr;

    int fd;
    FILE *fp;
    struct stat stbuf;

    fd = open("hello", O_RDONLY);
    assert(fd);
    fp = fdopen(fd, "rb");
    assert(fp);

    fstat(fd, &stbuf);
    unsigned char buf[stbuf.st_size];
    assert(fread(buf, 1, sizeof(buf), fp) == (unsigned long)stbuf.st_size);
    fclose(fp);

    printf("Elf file header(equivalent as readelf -h)\n");
    ehdr = (Elf64_Ehdr *)buf;
    dump_ehdr(ehdr);
}

適当にhello worldするhelloという実行可能ファイルを生成し、ダンププログラムを実行します。すると以下のような実行結果が得られました。

$ dump
Elf file header(equivalent as readelf -h)
  ehdr->e_ident = 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  ehdr->e_type = 2(0x2)
  ehdr->e_machine = 62(0x3e)
  ehdr->e_version = 1(0x1)
  ehdr->e_entry = 4195456(0x400480)
  ehdr->e_phoff = 64(0x40)
  ehdr->e_shoff = 6056(0x17a8)
  ehdr->e_flags = 0(0x0)
  ehdr->e_ehsize = 64(0x40)
  ehdr->e_phentsize = 56(0x38)
  ehdr->e_phnum = 10(0xa)
  ehdr->e_shentsize = 64(0x40)
  ehdr->e_shnum = 29(0x1d)
  ehdr->e_shstrndx = 26(0x1a)

readelfコマンドの実行結果と見比べてみます。readelfコマンドによるELFヘッダの出力は--file-header(-h)オプションでできます。

$ readelf -h hello
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400480
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6056 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 26

見比べてみると、大体同じような出力が得られていますが、自作のダンププログラムではClass、Data、Version、OS/ABI、ABI Versionが取得できていません。実はこれらは上のMagicに格納されており、マジックナンバーである0x7f 0x45 0x4c 0x64以降の情報がそれらに当たります。ちなみにELFファイルのマジックナンバーの0x45 0x4c 0x64はasciiコードでELFになります。0x7fは知りません。もう一度elf.hを見てみると、コメントからマジックナンバー(e_ident)に格納される値は以下のようになっていることが分かります。

/* Conglomeration of the identification bytes, for easy testing as a word.  */
#define	ELFMAG		"\177ELF"
#define	SELFMAG		4

#define EI_CLASS	4		/* File class byte index */
#define ELFCLASSNONE	0		/* Invalid class */
#define ELFCLASS32	1		/* 32-bit objects */
#define ELFCLASS64	2		/* 64-bit objects */
#define ELFCLASSNUM	3

#define EI_DATA		5		/* Data encoding byte index */
#define ELFDATANONE	0		/* Invalid data encoding */
#define ELFDATA2LSB	1		/* 2's complement, little endian */
#define ELFDATA2MSB	2		/* 2's complement, big endian */
#define ELFDATANUM	3

#define EI_VERSION	6		/* File version byte index */
					/* Value must be EV_CURRENT */

#define EI_OSABI	7		/* OS ABI identification */
#define ELFOSABI_NONE		0	/* UNIX System V ABI */
#define ELFOSABI_SYSV		0	/* Alias.  */
#define ELFOSABI_HPUX		1	/* HP-UX */
#define ELFOSABI_NETBSD		2	/* NetBSD.  */
#define ELFOSABI_GNU		3	/* Object uses GNU ELF extensions.  */
#define ELFOSABI_LINUX		ELFOSABI_GNU /* Compatibility alias.  */
#define ELFOSABI_SOLARIS	6	/* Sun Solaris.  */
#define ELFOSABI_AIX		7	/* IBM AIX.  */
#define ELFOSABI_IRIX		8	/* SGI Irix.  */
#define ELFOSABI_FREEBSD	9	/* FreeBSD.  */
#define ELFOSABI_TRU64		10	/* Compaq TRU64 UNIX.  */
#define ELFOSABI_MODESTO	11	/* Novell Modesto.  */
#define ELFOSABI_OPENBSD	12	/* OpenBSD.  */
#define ELFOSABI_ARM_AEABI	64	/* ARM EABI */
#define ELFOSABI_ARM		97	/* ARM */
#define ELFOSABI_STANDALONE	255	/* Standalone (embedded) application */

#define EI_ABIVERSION	8		/* ABI version */

#define EI_PAD		9		/* Byte index of padding bytes */

/* Legal values for e_version (version).  */

#define EV_NONE		0		/* Invalid ELF version */
#define EV_CURRENT	1		/* Current version */
#define EV_NUM		2

ClassはELFCLASS64=2、DataはELFDATA2LSB=1、VersionはEV_CURRENT=1、OS/ABIはELFOSABI_LINUX…ではなくELFOSABI_NONE=0、ABI Versionは…分からないです。とりあえず0でした。
仕様書を確認してみたんですが、そもそも仕様書のバージョン1.2の時点ではEI_VERSION以降は未定義なので独自拡張なのかな…。
ちなみに仕様書はこちらにあります。
https://refspecs.linuxfoundation.org/elf/elf.pdf

とりあえずreadelf -hが何をしているかが分かりました。このコマンドはELFファイルの頭から64バイトを読み込んで、各値を読みやすいように表示してくれているようです。
では、次にsection header tableとprogram header tableを見ていきます。ELFファイルヘッダのダンプ出力から、program header tableの開始位置はe_phoff=0x64、テーブルの要素数はe_phnum=10個であることが分かります。同様にsection header tableの開始位置はe_shoff=0x17a8、要素数はe_shnum=29個であることが分かります。これらを元に、先ほどのダンププログラムを拡張していきましょう。

#include <stdio.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <elf.h>

#define DUMP(x) do {printf("  " #x " = %u(0x%x)\n", (uint32_t)x, (uint32_t)x);} while(0);

void dump_ehdr(Elf64_Ehdr *ehdr){
    int i;
    printf("  ehdr->e_ident = ");
    for (i = 0; i < EI_NIDENT; i++) {
        printf("%02x ", ehdr->e_ident[i]);
    }
    printf("\n");
    DUMP(ehdr->e_type);
    DUMP(ehdr->e_machine);
    DUMP(ehdr->e_version);
    DUMP(ehdr->e_entry);
    DUMP(ehdr->e_phoff);
    DUMP(ehdr->e_shoff);
    DUMP(ehdr->e_flags);
    DUMP(ehdr->e_ehsize);
    DUMP(ehdr->e_phentsize);
    DUMP(ehdr->e_phnum);
    DUMP(ehdr->e_shentsize);
    DUMP(ehdr->e_shnum);
    DUMP(ehdr->e_shstrndx);
    printf("\n");
}

void dump_phdr(Elf64_Phdr *phdr, int e_phnum){
    int i;
    for (i = 0; i < e_phnum; i++, phdr++) {
        DUMP(phdr->p_type);
        DUMP(phdr->p_flags);
        DUMP(phdr->p_offset);
        DUMP(phdr->p_vaddr);
        DUMP(phdr->p_paddr);
        DUMP(phdr->p_filesz);
        DUMP(phdr->p_memsz);
        DUMP(phdr->p_align);
        printf("\n");
    }
    printf("\n");
}

void dump_shdr(Elf64_Shdr *shdr, int e_shnum){
    int i;
    for (i = 0; i < e_shnum; i++, shdr++) {
        DUMP(shdr->sh_name);
        DUMP(shdr->sh_type);
        DUMP(shdr->sh_flags);
        DUMP(shdr->sh_addr);
        DUMP(shdr->sh_offset);
        DUMP(shdr->sh_size);
        DUMP(shdr->sh_link);
        DUMP(shdr->sh_info);
        DUMP(shdr->sh_addralign);
        DUMP(shdr->sh_entsize);
        printf("\n");
    }
    printf("\n");
}

int main(void){
    Elf64_Ehdr *ehdr;
    Elf64_Phdr *phdr;
    Elf64_Shdr *shdr;
    int fd;
    FILE *fp;
    struct stat stbuf;

    fd = open("hello", O_RDONLY);
    assert(fd);
    fp = fdopen(fd, "rb");
    assert(fp);

    fstat(fd, &stbuf);
    unsigned char buf[stbuf.st_size];
    assert(fread(buf, 1, sizeof(buf), fp) == (unsigned long)stbuf.st_size);
    fclose(fp);

    printf("Elf file header(equivalent as readelf -h)\n");
    ehdr = (Elf64_Ehdr *)buf;
    dump_ehdr(ehdr);

    printf("Program header(equivalent as readelf -l)\n");
    phdr = (Elf64_Phdr *)(&buf[ehdr->e_phoff]);
    dump_phdr(phdr, ehdr->e_phnum);

    printf("Section header(equivalent as readelf -S)\n");
    shdr = (Elf64_Shdr *)(&buf[ehdr->e_shoff]);
    dump_shdr(shdr, ehdr->e_shnum);

    return 0;
}

実行結果が長くなってしまったのでgistに上げています。
ELFファイルのダンプ結果 · GitHub
readelfコマンドの結果と見比べていきます。program header tableはreadelf -l、section header tableはreadelf -Sで見ることができます。

$ readelf -l hello
Elf file type is EXEC (Executable file)
Entry point 0x400480
There are 10 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000230 0x0000000000000230  R E    8
  INTERP         0x0000000000000270 0x0000000000400270 0x0000000000400270
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000724 0x0000000000000724  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000230 0x0000000000000238  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x000000000000028c 0x000000000040028c 0x000000000040028c
                 0x0000000000000020 0x0000000000000020  R      4
  GNU_EH_FRAME   0x0000000000000604 0x0000000000400604 0x0000000000400604
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1
  PAX_FLAGS      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000         8

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 
   09     
$ readelf -S hello
There are 29 section headers, starting at offset 0x17a8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400270  00000270
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             000000000040028c  0000028c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .gnu.hash         GNU_HASH         00000000004002b0  000002b0
       000000000000001c  0000000000000000   A       4     0     8
  [ 4] .dynsym           DYNSYM           00000000004002d0  000002d0
       0000000000000060  0000000000000018   A       5     1     8
  [ 5] .dynstr           STRTAB           0000000000400330  00000330
       000000000000003d  0000000000000000   A       0     0     1
  [ 6] .gnu.version      VERSYM           000000000040036e  0000036e
       0000000000000008  0000000000000002   A       4     0     2
  [ 7] .gnu.version_r    VERNEED          0000000000400378  00000378
       0000000000000020  0000000000000000   A       5     1     8
  [ 8] .rela.dyn         RELA             0000000000400398  00000398
       0000000000000018  0000000000000018   A       4     0     8
  [ 9] .rela.plt         RELA             00000000004003b0  000003b0
       0000000000000048  0000000000000018  AI       4    11     8
  [10] .init             PROGBITS         00000000004003f8  000003f8
       000000000000001a  0000000000000000  AX       0     0     4
  [11] .plt              PROGBITS         0000000000400420  00000420
       0000000000000040  0000000000000010  AX       0     0     16
  [12] .text             PROGBITS         0000000000400460  00000460
       0000000000000181  0000000000000000  AX       0     0     16
  [13] .fini             PROGBITS         00000000004005e4  000005e4
       0000000000000009  0000000000000000  AX       0     0     4
  [14] .rodata           PROGBITS         00000000004005f0  000005f0
       0000000000000012  0000000000000000   A       0     0     4
  [15] .eh_frame_hdr     PROGBITS         0000000000400604  00000604
       0000000000000034  0000000000000000   A       0     0     4
  [16] .eh_frame         PROGBITS         0000000000400638  00000638
       00000000000000ec  0000000000000000   A       0     0     8
  [17] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [18] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000000  WA       0     0     8
  [19] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       5     0     8
  [21] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000030  0000000000000008  WA       0     0     8
  [23] .data             PROGBITS         0000000000601030  00001030
       0000000000000010  0000000000000000  WA       0     0     8
  [24] .bss              NOBITS           0000000000601040  00001040
       0000000000000008  0000000000000000  WA       0     0     1
  [25] .comment          PROGBITS         0000000000000000  00001040
       000000000000002a  0000000000000001  MS       0     0     1
  [26] .shstrtab         STRTAB           0000000000000000  0000106a
       00000000000000f5  0000000000000000           0     0     1
  [27] .symtab           SYMTAB           0000000000000000  00001160
       00000000000004e0  0000000000000018          28    32     8
  [28] .strtab           STRTAB           0000000000000000  00001640
       0000000000000166  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

中々見比べるのが大変ですね。まずはprogram header tableから見ていきます。ちょっと量が多いので、一番上の出力だけを見て行きましょう。

  phdr->p_type = 6(0x6)
  phdr->p_flags = 5(0x5)
  phdr->p_offset = 64(0x40)
  phdr->p_vaddr = 4194368(0x400040)
  phdr->p_paddr = 4194368(0x400040)
  phdr->p_filesz = 560(0x230)
  phdr->p_memsz = 560(0x230)
  phdr->p_align = 8(0x8)

以下はreadelf -l helloの結果から対応してそうな値を抜粋したものです。

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x0000000000000230 0x0000000000000230  R E    8

先述したように、program header tableに対応する構造体はElf64_Phdrになります。Elf64_Phdrのメンバのうち、p_typeとp_flagsは以下の値で定義されます。

/* Legal values for p_type (segment type).  */

#define	PT_NULL		0		/* Program header table entry unused */
#define PT_LOAD		1		/* Loadable program segment */
#define PT_DYNAMIC	2		/* Dynamic linking information */
#define PT_INTERP	3		/* Program interpreter */
#define PT_NOTE		4		/* Auxiliary information */
#define PT_SHLIB	5		/* Reserved */
#define PT_PHDR		6		/* Entry for header table itself */
#define PT_TLS		7		/* Thread-local storage segment */
#define	PT_NUM		8		/* Number of defined types */
#define PT_LOOS		0x60000000	/* Start of OS-specific */
#define PT_GNU_EH_FRAME	0x6474e550	/* GCC .eh_frame_hdr segment */
#define PT_GNU_STACK	0x6474e551	/* Indicates stack executability */
#define PT_GNU_RELRO	0x6474e552	/* Read-only after relocation */
#define PT_LOSUNW	0x6ffffffa
#define PT_SUNWBSS	0x6ffffffa	/* Sun Specific segment */
#define PT_SUNWSTACK	0x6ffffffb	/* Stack segment */
#define PT_HISUNW	0x6fffffff
#define PT_HIOS		0x6fffffff	/* End of OS-specific */
#define PT_LOPROC	0x70000000	/* Start of processor-specific */
#define PT_HIPROC	0x7fffffff	/* End of processor-specific */

/* Legal values for p_flags (segment flags).  */

#define PF_X		(1 << 0)	/* Segment is executable */
#define PF_W		(1 << 1)	/* Segment is writable */
#define PF_R		(1 << 2)	/* Segment is readable */
#define PF_MASKOS	0x0ff00000	/* OS-specific */
#define PF_MASKPROC	0xf0000000	/* Processor-specific */

(これもまた仕様書に載ってない値がちらほら…)
p_type==6ということはPT_PHDRですね。また、p_flags == ( (1<<0) | (1<<2) )であるため、executableかつreadableであることが分かります。readelfコマンドの出力結果を見るとFlagsのところにR Eと書いてあるので、きっとReadableとExecutableでしょう。その他の値に関しても同じ出力が得られています。
この出力結果から、program header tableに格納されている種類や値が分かりました。tableというからには、ファイルが参照する先には何か値が入っているのでしょう。(少しメタりますがこのPT_PHDRでは面白くないので、次の値のPT_INTERPを見てみます)。

  phdr->p_type = 3(0x3)
  phdr->p_flags = 4(0x4)
  phdr->p_offset = 624(0x270)
  phdr->p_vaddr = 4194928(0x400270)
  phdr->p_paddr = 4194928(0x400270)
  phdr->p_filesz = 28(0x1c)
  phdr->p_memsz = 28(0x1c)
  phdr->p_align = 1(0x1)

readelf -l helloの出力の該当しそうな値は以下の結果です。(一部抜粋)

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  INTERP         0x0000000000000270 0x0000000000400270 0x0000000000400270
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

readelfコマンドの方では[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]という出力が得られましたが、そんな値はElf64_Phdrには格納されていません。どういうことでしょうか。
とりあえずp_offset=0x270という点と、p_filesz=0x1cという点に注目して、helloファイル上の値を確認してみます。odコマンドを使い、16進数とascii表示でダンプしていきます。

$ od -Ax -tx1c -j 0x270 -N 0x1c hello
000270  2f  6c  69  62  36  34  2f  6c  64  2d  6c  69  6e  75  78  2d
         /   l   i   b   6   4   /   l   d   -   l   i   n   u   x   -
000280  78  38  36  2d  36  34  2e  73  6f  2e  32  00
         x   8   6   -   6   4   .   s   o   .   2  \0
00028c

readelfで得られた出力と同じ文字列が格納されていることが分かります。このことから、readelfコマンドは自動的にp_typeがPT_INTERPであるときはテーブルの参照先を確認して、その文字列を表示してくれることが分かりました。
ちなみにPT_INTERPの詳細については分からないので飛ばします。動的リンクを処理するインタプリタが入っているとのことです。

次に、section header tableを見てみます。program header tableと同様に一番目の値だけを比較して…と思ったんですが、全て0が格納されていて見比べようが無いので二番目の値を見ていきます。

  shdr->sh_name = 27(0x1b)
  shdr->sh_type = 1(0x1)
  shdr->sh_flags = 2(0x2)
  shdr->sh_addr = 4194928(0x400270)
  shdr->sh_offset = 624(0x270)
  shdr->sh_size = 28(0x1c)
  shdr->sh_link = 0(0x0)
  shdr->sh_info = 0(0x0)
  shdr->sh_addralign = 1(0x1)
  shdr->sh_entsize = 0(0x0)
$ readelf -S hello
  [ 1] .interp           PROGBITS         0000000000400270  00000270
       000000000000001c  0000000000000000   A       0     0     1

まずsh_nameですが、elf.hのコメントを見るとSection name (string tbl index)と書いてあり、string tableという新しいテーブルが関係してくるようです。これは、ELF headerのe_shstrndxが指し示す位置に格納されています。尚、e_shstrndxが指し示す位置というのはファイルの先頭からのオフセットではなく、section header tableのインデックスに対応しています。早速、ehdr->e_shstrndx = 26(0x1a)という情報を元に26番目のsection header tableを見てみましょう。

  shdr->sh_name = 17(0x11)
  shdr->sh_type = 3(0x3)
  shdr->sh_flags = 0(0x0)
  shdr->sh_addr = 0(0x0)
  shdr->sh_offset = 4202(0x106a)
  shdr->sh_size = 245(0xf5)
  shdr->sh_link = 0(0x0)
  shdr->sh_info = 0(0x0)
  shdr->sh_addralign = 1(0x1)
  shdr->sh_entsize = 0(0x0)

sh_offsetは0x106a、sh_sizeは0xf5ということが分かったのでodでダンプします。

$ od -Ax -c -j 0x106a -N 0xf5 hello
00106a  \0   .   s   y   m   t   a   b  \0   .   s   t   r   t   a   b
00107a  \0   .   s   h   s   t   r   t   a   b  \0   .   i   n   t   e
00108a   r   p  \0   .   n   o   t   e   .   A   B   I   -   t   a   g
00109a  \0   .   g   n   u   .   h   a   s   h  \0   .   d   y   n   s
0010aa   y   m  \0   .   d   y   n   s   t   r  \0   .   g   n   u   .
0010ba   v   e   r   s   i   o   n  \0   .   g   n   u   .   v   e   r
0010ca   s   i   o   n   _   r  \0   .   r   e   l   a   .   d   y   n
0010da  \0   .   r   e   l   a   .   p   l   t  \0   .   i   n   i   t
0010ea  \0   .   t   e   x   t  \0   .   f   i   n   i  \0   .   r   o
0010fa   d   a   t   a  \0   .   e   h   _   f   r   a   m   e   _   h
00110a   d   r  \0   .   e   h   _   f   r   a   m   e  \0   .   i   n
00111a   i   t   _   a   r   r   a   y  \0   .   f   i   n   i   _   a
00112a   r   r   a   y  \0   .   j   c   r  \0   .   d   y   n   a   m
00113a   i   c  \0   .   g   o   t  \0   .   g   o   t   .   p   l   t
00114a  \0   .   d   a   t   a  \0   .   b   s   s  \0   .   c   o   m
00115a   m   e   n   t  \0
00115f

NULL文字で区切って要素名が書かれていることが分かります。これを元にプログラムを拡張しましょう。ちなみにsh_nameはstring tableの先頭からのオフセットを意味します。

void dump_stringtbl(unsigned char *str, Elf64_Shdr *shdr){
    unsigned char *tbl_head = &str[shdr->sh_offset];
    unsigned long total_len = 0;

    while(total_len < shdr->sh_size){
        printf("  %03lu: %s\n", (&tbl_head[total_len] - tbl_head), &tbl_head[total_len]);
        total_len += strlen((char *)&tbl_head[total_len]) + 1;
    }
    printf("\n");
}

int main(void){
    Elf64_Ehdr *ehdr;
    Elf64_Phdr *phdr;
    Elf64_Shdr *shdr;
    int fd;
    FILE *fp;
    struct stat stbuf;

    fd = open("hello", O_RDONLY);
    assert(fd);
    fp = fdopen(fd, "rb");
    assert(fp);

    fstat(fd, &stbuf);
    unsigned char buf[stbuf.st_size];
    assert(fread(buf, 1, sizeof(buf), fp) == (unsigned long)stbuf.st_size);
    fclose(fp);

    // 〜(中略)〜
    ehdr = (Elf64_Ehdr *)buf;
    shdr = (Elf64_Shdr *)(&buf[ehdr->e_shoff]);

    printf("String table\n");
    dump_stringtbl(buf, &shdr[ehdr->e_shstrndx]);

    return 0;
}

出力結果を見てみましょう。

$ ./dump
(中略)
String table
  000: 
  001: .symtab
  009: .strtab
  017: .shstrtab
  027: .interp
  035: .note.ABI-tag
  049: .gnu.hash
  059: .dynsym
  067: .dynstr
  075: .gnu.version
  088: .gnu.version_r
  103: .rela.dyn
  113: .rela.plt
  123: .init
  129: .text
  135: .fini
  141: .rodata
  149: .eh_frame_hdr
  163: .eh_frame
  173: .init_array
  185: .fini_array
  197: .jcr
  202: .dynamic
  211: .got
  216: .got.plt
  225: .data
  231: .bss
  236: .comment

section header tableの二番目の値を再掲します。

  shdr->sh_name = 27(0x1b)
  shdr->sh_type = 1(0x1)
  shdr->sh_flags = 2(0x2)
  shdr->sh_addr = 4194928(0x400270)
  shdr->sh_offset = 624(0x270)
  shdr->sh_size = 28(0x1c)
  shdr->sh_link = 0(0x0)
  shdr->sh_info = 0(0x0)
  shdr->sh_addralign = 1(0x1)
  shdr->sh_entsize = 0(0x0)

sh_nameに27が格納されていることから、この値の名前は.interpであることが確認できました。これでreadelf -S helloと同等の結果が得られていることが分かりました。
次に、sh_typeとsh_flagsはelf.hに以下のように記述されています。

/* Legal values for sh_type (section type).  */

#define SHT_NULL	  0		/* Section header table entry unused */
#define SHT_PROGBITS	  1		/* Program data */
#define SHT_SYMTAB	  2		/* Symbol table */
#define SHT_STRTAB	  3		/* String table */
#define SHT_RELA	  4		/* Relocation entries with addends */
#define SHT_HASH	  5		/* Symbol hash table */
#define SHT_DYNAMIC	  6		/* Dynamic linking information */
#define SHT_NOTE	  7		/* Notes */
#define SHT_NOBITS	  8		/* Program space with no data (bss) */
#define SHT_REL		  9		/* Relocation entries, no addends */
#define SHT_SHLIB	  10		/* Reserved */
#define SHT_DYNSYM	  11		/* Dynamic linker symbol table */
#define SHT_INIT_ARRAY	  14		/* Array of constructors */
#define SHT_FINI_ARRAY	  15		/* Array of destructors */
#define SHT_PREINIT_ARRAY 16		/* Array of pre-constructors */
#define SHT_GROUP	  17		/* Section group */
#define SHT_SYMTAB_SHNDX  18		/* Extended section indeces */
#define	SHT_NUM		  19		/* Number of defined types.  */
#define SHT_LOOS	  0x60000000	/* Start OS-specific.  */
#define SHT_GNU_ATTRIBUTES 0x6ffffff5	/* Object attributes.  */
#define SHT_GNU_HASH	  0x6ffffff6	/* GNU-style hash table.  */
#define SHT_GNU_LIBLIST	  0x6ffffff7	/* Prelink library list */
#define SHT_CHECKSUM	  0x6ffffff8	/* Checksum for DSO content.  */
#define SHT_LOSUNW	  0x6ffffffa	/* Sun-specific low bound.  */
#define SHT_SUNW_move	  0x6ffffffa
#define SHT_SUNW_COMDAT   0x6ffffffb
#define SHT_SUNW_syminfo  0x6ffffffc
#define SHT_GNU_verdef	  0x6ffffffd	/* Version definition section.  */
#define SHT_GNU_verneed	  0x6ffffffe	/* Version needs section.  */
#define SHT_GNU_versym	  0x6fffffff	/* Version symbol table.  */
#define SHT_HISUNW	  0x6fffffff	/* Sun-specific high bound.  */
#define SHT_HIOS	  0x6fffffff	/* End OS-specific type */
#define SHT_LOPROC	  0x70000000	/* Start of processor-specific */
#define SHT_HIPROC	  0x7fffffff	/* End of processor-specific */
#define SHT_LOUSER	  0x80000000	/* Start of application-specific */
#define SHT_HIUSER	  0x8fffffff	/* End of application-specific */

/* Legal values for sh_flags (section flags).  */

#define SHF_WRITE	     (1 << 0)	/* Writable */
#define SHF_ALLOC	     (1 << 1)	/* Occupies memory during execution */
#define SHF_EXECINSTR	     (1 << 2)	/* Executable */
#define SHF_MERGE	     (1 << 4)	/* Might be merged */
#define SHF_STRINGS	     (1 << 5)	/* Contains nul-terminated strings */
#define SHF_INFO_LINK	     (1 << 6)	/* `sh_info' contains SHT index */
#define SHF_LINK_ORDER	     (1 << 7)	/* Preserve order after combining */
#define SHF_OS_NONCONFORMING (1 << 8)	/* Non-standard OS specific handling
					   required */
#define SHF_GROUP	     (1 << 9)	/* Section is member of a group.  */
#define SHF_TLS		     (1 << 10)	/* Section hold thread-local data.  */
#define SHF_COMPRESSED	     (1 << 11)	/* Section with compressed data. */
#define SHF_MASKOS	     0x0ff00000	/* OS-specific.  */
#define SHF_MASKPROC	     0xf0000000	/* Processor-specific */
#define SHF_ORDERED	     (1 << 30)	/* Special ordering requirement
					   (Solaris).  */
#define SHF_EXCLUDE	     (1U << 31)	/* Section is excluded unless
					   referenced or allocated (Solaris).*/

sh_typeはSHT_PROGBITS=1ですし、sh_flagsは0x1なのでSHF_ALLOCと同値です。readelf -S helloの出力を見てもAllocateのものと思われるAが書いてあるので、これらに関しても同等の結果が得られています。
次に、sh_infoとsh_linkですが、これらの値に関してはelf.hのコメントを読んでも該当する値が見当たりませんでした。仕様書を確認した所、sh_typeによって異なる値が設定されるらしく、今回ではsh_type=0であり、コメントにはSection header table entry unusedと書かれているので、sh_infoとsh_linkに関しても使用していないものだと思われます。
次にsh_sizeとsh_entsizeという2つの値ですが、sh_sizeは今までも使ってきたようにセクション自体のサイズをバイト単位で表したものです。sh_entsizeはシンボルテーブルのような固定長のエントリを持つテーブルが、各エントリのサイズを表すために使っているそうです。仕様書をそのまま訳しただけなので、次回以降に実際にシンボルテーブルを見てみるときに確認しましょう。

最初に想定していた5倍くらいの文量になりましたが、以上でreadelf -h、-l、-Sと同じ出力をするダンププログラムの作成と解説を終わります。以下まとめです。
readelfコマンドはELFファイルの各フィールドを確認して、人間が見やすいように整形してくれるコマンドです(今更)。readelf -hではELFファイルの先頭64バイトを元に整形結果を出力します。readelf -lではまずreadelf -hと同様にELFファイルの先頭64バイトを確認し、ELFファイルの先頭からe_phoffsetバイト目をプログラムヘッダテーブルとして整形して出力します。readelf -Sも同様にELFファイルの先頭64バイトを確認し、ELFファイルの先頭からe_shoffsetバイト目をセクションヘッダテーブルとして整形して出力します。この時、セクションヘッダテーブルのe_shstrndx番目にあるストリングテーブルを確認して、各セクションに設定されたsh_nameに該当する名前を整形して表示しています。
まだまだ分からないことがたくさんあるので、おいおい見ていきたいと思います。