飛び入りです。Rubyアドベントカレンダーその2 17日目の記事です。昨日はなんとなく似たような(?)、 Rubyの8進数と2進数の構文エラー文の違い の話です。
今日はRubyで構造体、と言うか、バイナリパックされた構造体を扱う話の触りをします。class Structの話はしない。
さてみなさんは String#unpack/Array#pack
を使ってますか?
すこく正直な話をすると、おそらく多くの方は使っていなくて、使っている方も大半は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のソースコードの これ や あれ を開いときましょう。
これによると、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 = 3
はET_DYN
に対応 - 3番目
e_machine = 62
はEM_X86_64
に対応 - 4番目
e_version = 1
はEV_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 = 64
、 e_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つ目がちゃんと対応した値になっていると判断できます。
結論
- Rubyの
String#unpack/Array#pack
を用い、構造体を表現したバイナリをpack/unpackできる。 - ELFヘッダは構造体にunpackすればパーズできる。
- Rubyでバイナリ解析すると便利。
フィヨルド、最終課題でreadelf
コマンド自作はどうでしょう
Rubyのpackについて、少しでも謎が解ければ幸甚です。
参考になるサイト
同じようなことをGoで行った例です。Rubyもまけないぞ(?)
ということでRubyアドカレのその2、明日も書き手募集中の模様。