ローファイ日記

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

メモリの上に置かれているRubyの文字列を覗く

これもフィヨルドブートキャンプの生徒さんの質問からふと思いついた、ちょっとした遊びですが。

(そして、書いてある内容に誤解があったら優しく教えてください)

p Object.new
=> #<Object:0x000055959ddf1910>

Rubyのオブジェクトのinspect表示のデフォルトで出てくる、この16進数は、このオブジェクトが置かれているメモリアドレスのことだと知られている。

では、実際にこのメモリアドレスにオブジェクトが置かれていることを確かめるには?

さて、以下のコードはLinuxで動かすことにする。

String オブジェクトで試してみる。と言っても、StringのinspectはObjectに定義されたものではなく、自分のクラスで定義しているので、まずはそれを「無効にする」。以下のような方法で Object#inspect を呼ぶように変更できる。

class String
  # 元に戻せるよう、本来のinspectを別名で保存
  alias orig_inspect inspect
  def inspect
    super
  end
end

String のinspect表示が変わっているのを確かめる。

str = "Hola, mundo"
=> #<String:0x000055959d972038>

このアドレスにある自分のメモリの状況を見てみたい。Linuxであれば、 /proc/self/mem を読み込むことで確認ができる。

このprocってなんだろう? と詳しく知りたい向きには man 5 proc などを読んでもらうこととして、 /proc というファイルシステムの下からプロセスの情報が取得できるというぐらいの理解で一旦は先に進もう。

ということで /proc/self/mem を読み込んで 0x000055959d972038 分だけseekする。Rubyだとこの表現のまま数値と認識してくれるので便利だね。

mem = open("/proc/self/mem")
mem.seek 0x000055959d972038

この先の作業がわかりづらいので、String#inspectの定義を元に戻す。

class String
  alias inspect orig_inspect
end

さて、メモリからとりあえず32バイト(なお、この数値は勘で決めました)ほど読み込んでみた。

dump = mem.read 32
=> "e\xC0R\x00\x00\x00\x00\x00`hT\x9D\x95U\x00\x00Hola, mundo\x00\x00\x00\x00\x00"

なんとなく、先ほどの文字列 Hola, mundo が格納されているような気がするが、実際どういうレイアウトになっているかというと。

Ruby 3.0.2 では文字列を表す構造体 RString次の定義:

struct RString {
    struct RBasic basic;
    union {
        struct {
            long len;
            char *ptr;
            union {
                long capa;
                VALUE shared;
            } aux;
        } heap;
        char ary[RSTRING_EMBED_LEN_MAX + 1];
    } as;
};

RBasicは次の定義:

struct
RUBY_ALIGNAS(SIZEOF_VALUE)
RBasic {
    VALUE flags;                /**< @see enum ::ruby_fl_type. */
    const VALUE klass;

#ifdef __cplusplus
  // ... は関係ないので飛ばします
#endif
};

VALUEだいたい uintptr_t のことらしい。

typedef uintptr_t VALUE;

ビルドの環境によって当然若干変わってくるが、筆者の x86-64Linux環境ではこの定義と考えて良いだろう。

ということは、メモリから取り出したバイナリ列を、とりあえず以下の要領で unpack すれば。

# VALUE VALUE char[] として unpack する
dump.unpack "Q! Q! Z*"
=> [5423205, 94101078042720, "Hola, mundo"]

... というようにメモリ上の文字列を取り出せた。


ちなみに、 RSTRING_EMBED_LEN_MAX という定数があることから、ある程度以上長い文字列だとRString構造体の中には収まらないことが想像される。

実際にやってみよう。

class String; def inspect; super; end; end
str2 = "very long string " * 100
=> #<String:0x000055959de10180>
# 戻す
class String; alias inspect orig_inspect; end

0x000055959de10180 を読み込む。

mem.close
mem = open("/proc/self/mem")
mem.seek 0x000055959de10180
dump = mem.read 32
=> "e P\x00\x00\x00\x00\x00`hT\x9D\x95U\x00\x00\xA4\x06\x00\x00\x00\x00\x00\x00`\xE2\xE7\x9D\x95U\x00\x00"

上述した RString, RBasic の定義を参考にして、こういうレイアウトだとしてunpackしてみる。

struct _RString {
  VALUE flags;
  VALUE klass;
  struct {
    long len;
    char *ptr;
    // ...
  } heap
}
dump.unpack "Q! Q! l! Q!"
=> [5251173, 94101078042720, 1700, 94101087707744]

1700 というのが文字列の長さにちゃんと対応している。

str2.size
=> 1700

で、アドレス 94101087707744 *1から 1700 バイト読み込み、そこに当該の文字列がいることを確認することができる。

mem.seek 94101087707744
mem.read 1700
=> "very long string very long string very long string... "

まとめ

  • Rubyのinspect表示に出てくる16進数は、多くの場合メモリアドレスを表す*2
  • /proc/self/mem から自分のメモリを確認する遊びができる

*1:"0x%016x" % 94101087707744 => 0x000055959de7e260 で多分これもヒープのどこか。

*2:Integerの場合などそうでないこともあるぞ