ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Rubyで構造体を扱う(ただし、Cの)。あとELFの話。

飛び入りです。Rubyアドベントカレンダーその2 17日目の記事です。昨日はなんとなく似たような(?)、 Rubyの8進数と2進数の構文エラー文の違い の話です。

qiita.com

今日はRubyで構造体、と言うか、バイナリパックされた構造体を扱う話の触りをします。class Structの話はしない。

さてみなさんは String#unpack/Array#pack を使ってますか?

docs.ruby-lang.org

すこく正直な話をすると、おそらく多くの方は使っていなくて、使っている方も大半はbase64文字列を生成する m フォーマットぐらいしか使っていないのではないかと推測しています。

["Hello World"].pack('m')
#=> "SGVsbG8gV29ybGQ=\n"

ぼくも今まではそうだったのですが、最近は他のフォーマットも急に使うようになりました。というのも、Cで扱う構造体、実質バイナリ列を簡単にRubyの世界に持っていく際に便利な代物だからです。去年の終わりからずっと ffiをガッツリ使ったgem の開発に勤しんでいるので、やや詳しくなってしまった。

ということで今日は構造体とffiとpack/unpackの話をしていきます。

今回の環境

Cの構造体をRubyで取り出そう

Cの構造体はどう言う場合に出くわすかというと、主に、ffiでCの関数を扱いたい場合、複雑な関数を呼びたくなったら必要になります。

Rubyの標準添付(2.7段階では)のgemであるfiddleを使います。ライブラリコールとしての uname(2)Rubyスクリプトから呼んでみましょう。

まずlibcのハンドルを取ります。

require 'fiddle'
libc = Fiddle::Handle.new('libc.so.6')

libcの uname のアドレスから関数を取り出します。定義は man 2 uname の通りですが、 void* を受け取って int を返す関数とします。

uname = Fiddle::Function.new(libc.sym('uname'), [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)

さて、引数の struct utsname *buf 相当のポインタを表現するFiddle::Pointerのインスタンスを作りますが、サイズは、 カーネルのコード などを眺めて多分 char[65] のメンバが6つあると考えてそのサイズを確保します。

uts = Fiddle::Pointer.malloc(65 * 6)

これを引数に呼び出す。

uname.call(uts)
#=> 0

uts には値が埋まっています。 uts.to_str で確認できます。なお to_s だと最初の \0 に突き当たったところまでしか出してくれません。

uts.to_str
=> "Linux\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00ubuntu2004.localdomain\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x005.8.0-29-generic\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00#31~20.04.1-Ubuntu SMP Fri Nov 6 16:10:42 UTC 2020\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00x86_64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00(none)\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00"

これをRubyで扱いやすくしましょう、以下のようにフォーマットを指定し、「char[65] のメンバが6つある構造体」としてunpackします。

pp uts.to_str.unpack("Z65 Z65 Z65 Z65 Z65 Z65")
["Linux",
 "ubuntu2004.localdomain",
 "5.8.0-29-generic",
 "#31~20.04.1-Ubuntu SMP Fri Nov 6 16:10:42 UTC 2020",
 "x86_64",
 "(none)"]

はいOKそう。

他、例えば clock_gettime(2) なんかでも値が取れます。ナノ秒まで取れますね。

require 'fiddle'
libc = Fiddle::Handle.new('libc.so.6')
f = Fiddle::Function.new(
  libc.sym('clock_gettime'),
  [Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
)

p = Fiddle::Pointer.malloc(16)
f.call(Process::CLOCK_REALTIME, p)  
p.to_str.unpack("l! l!")
#=> [1608116263, 831128860]

無論、逆に構造体を表現する文字列にパックすることも可能です。とはいえ、この辺りのシステムコール呼び出しは、普通Rubyの標準ライブラリなどが提供するクラス・メソッドを使うとは思いますが*1...。

ただ、るりまの説明にもある通り、例えばIO#ioctlなどのメソッドの引数にはパックされた構造体を渡す時があるので、そう言う場合には自分でバイナリ列を用意することになります。ioctl(2)なんか直接呼ばねーよ、と言う意見には、「そうだね」とお返事します。

あと、 Fiddle::Importer#struct のようなもう少し高レベルなAPIを使うと意識しなくても済むようにはなっていますね...。

ELFバイナリを構造体にunpackする

もう少し実用的(?)な例としてはELFの情報のパーズがあります。

ここから先の記事は以下の man 5 elf と睨めっこしながら読むといいです。あと、お手元に elf.h などを用意してください。Linuxソースコードこれあれ を開いときましょう。

man7.org

これによると、ELFヘッダの構造体は以下のようになります。一旦、64bitアーキテクチャに決め打ちします。

#define EI_NIDENT 16

typedef struct {
    unsigned char e_ident[EI_NIDENT];
    uint16_t      e_type;
    uint16_t      e_machine;
    uint32_t      e_version;
    Elf64_Addr     e_entry;
    Elf64_Off      e_phoff;
    Elf64_Off      e_shoff;
    uint32_t      e_flags;
    uint16_t      e_ehsize;
    uint16_t      e_phentsize;
    uint16_t      e_phnum;
    uint16_t      e_shentsize;
    uint16_t      e_shnum;
    uint16_t      e_shstrndx;
} Elf64_Ehdr;

Elf64_* は以下のような定義です。

typedef __u64   Elf64_Addr;
typedef __u16  Elf64_Half;
typedef __s16  Elf64_SHalf;
typedef __u64  Elf64_Off;
typedef __s32  Elf64_Sword;
typedef __u32  Elf64_Word;
typedef __u64  Elf64_Xword;
typedef __s64  Elf64_Sxword;

この辺の情報を参考にpackフォーマット文字列を作ると以下のようになると思われます。

"Z16 S! S! I! L! L! L! I! S! S! S! S! S! S!"

さて、いわゆるELF形式のバイナリの先頭はこの構造体にunpackできるようになっています。bashで試してみましょう。

b = File.read("/bin/bash")
b.unpack "Z16 S! S! I! L! L! L! I! S! S! S! S! S! S!"
=> ["\x7FELF\x02\x01\x01", 3, 62, 1, 197680, 64, 1181528, 0, 64, 56, 13, 64, 30, 29]

ここで:

  • 2番目 e_type = 3ET_DYN に対応
  • 3番目 e_machine = 62EM_X86_64 に対応
  • 4番目 e_version = 1EV_CURRENT に対応
  • 5番目 e_entry は、 "0x%x" % 197680 => "0x30430"

です。 readelf の結果で答え合わせしましょう。どうですか? ちゃんとパーズできていそうです。

$ readelf -h /bin/bash
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:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x30430
  Start of program headers:          64 (bytes into file)
  Start of section headers:          1181528 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

プログラムヘッダもunpackする

さらに、プログラムヘッダーにも潜ってみます。上記の結果の通り e_phoff = 64e_phnum = 13 です。bashのプログラムのバイナリの、65バイト目から13個のELFプログラムヘッダがあるそうです。

ELFプログラムヘッダ構造体は、manによると以下。

typedef struct {
    uint32_t   p_type;
    uint32_t   p_flags;
    Elf64_Off  p_offset;
    Elf64_Addr p_vaddr;
    Elf64_Addr p_paddr;
    uint64_t   p_filesz;
    uint64_t   p_memsz;
    uint64_t   p_align;
} Elf64_Phdr;

packフォーマットでは "I! I! L! L! L! L! L! L!" です。サイズは e_phentsize のはずですね。ついでにRuby上で確認しておきましょう。

[0,0,0,0,0,0,0,0].pack("I! I! L! L! L! L! L! L!").size
#=> 56

ではbashのプログラムヘッダを読み取ります。最初はこういう感じ。

f = File.open("/bin/bash")
f.seek 64
b = f.read(56)
b.unpack("I! I! L! L! L! L! L! L!")
=> [6, 4, 64, 64, 64, 728, 728, 8]

2つ目は、さらに56バイト読み取りをすればOKですね。

f.read(56)
  .unpack("I! I! L! L! L! L! L! L!")
=> [3, 4, 792, 792, 792, 28, 28, 1]

いったん1番目 p_type と2番目 p_flags の値に注目します。取りうる定数はこんな感じのようです。

#define PT_NULL    0
#define PT_LOAD    1
#define PT_DYNAMIC 2
#define PT_INTERP  3
#define PT_NOTE    4
#define PT_SHLIB   5
#define PT_PHDR    6
#define PT_TLS     7               /* Thread local storage segment */
// イカ続く

//...
#define PF_R       0x4
#define PF_W       0x2
#define PF_X       0x1

1要素目は「PT_PHDR, PF_R, ...」、2要素目は「PT_INTERP, PF_R, ...」と思われます。答え合わせはやっぱり readelf で...。

$ readelf -l /bin/bash

Elf file type is DYN (Shared object file)
Entry point 0x30430
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x000000000002ce70 0x000000000002ce70  R      0x1000
  LOAD           0x000000000002d000 0x000000000002d000 0x000000000002d000
                 0x00000000000b0705 0x00000000000b0705  R E    0x1000
  LOAD           0x00000000000de000 0x00000000000de000 0x00000000000de000
                 0x0000000000036198 0x0000000000036198  R      0x1000
...

最初から1つ目、2つ目がちゃんと対応した値になっていると判断できます。

結論

  • RubyString#unpack/Array#pack を用い、構造体を表現したバイナリをpack/unpackできる。
  • ELFヘッダは構造体にunpackすればパーズできる。
  • Rubyでバイナリ解析すると便利。
  • フィヨルド、最終課題で readelf コマンド自作はどうでしょう

Rubyのpackについて、少しでも謎が解ければ幸甚です。

参考になるサイト

drumato.hatenablog.com

同じようなことをGoで行った例です。Rubyもまけないぞ(?)

ということでRubyアドカレのその2、明日も書き手募集中の模様。

*1:Etc.#uname や Process.#clock_gettime がありますね