2007年11月

このエントリーを含むブックマーク 2007年11月22日

次のようなプログラム test.c について考える:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>

struct test {
    int32_t len;
    int8_t buf[16];
};

int main(int argc, char *argv[]) {
    struct test *p = malloc(sizeof(struct test));
    int8_t buf[16];
    p->len = sizeof(p->buf);
    bzero(p->buf, p->len);
    printf("0x%lX-0x%lX => 0x%lX\n",
	   (long)p->buf, (long)p->buf+p->len-1, (long)buf);
    bcopy(p->buf, buf, p->len);
    free(p);
    return 0;
}

malloc(3) で確保した領域のうち、 16 byte を bcopy(3) でコピーするだけの極めて単純なプログラムであり、 特に問題はないように見える。

ところが memory debugging tool Valgrind を使って検証してみると、 x86_64 Linux だと次のようなエラーが出てしまう。

sag16:/home/sengoku/tmp % cc -O -Wall test.c
sag16:/home/sengoku/tmp % valgrind ./a.out
==19008== Memcheck, a memory error detector.
==19008== Copyright (C) 2002-2006, and GNU GPL'd, by Julian Seward et al.
==19008== Using LibVEX rev 1658, a library for dynamic binary translation.
==19008== Copyright (C) 2004-2006, and GNU GPL'd, by OpenWorks LLP.
==19008== Using valgrind-3.2.1-Debian, a dynamic binary instrumentation framework.
==19008== Copyright (C) 2000-2006, and GNU GPL'd, by Julian Seward et al.
==19008== For more details, rerun with: -v
==19008== 
0x4D5C034-0x4D5C043 => 0x7FF000750
==19008== Invalid read of size 8
==19008==    at 0x4B9326B: (within /lib/libc-2.3.6.so)
==19008==    by 0x4B92C06: bcopy (in /lib/libc-2.3.6.so)
==19008==    by 0x4005BD: main (in /home/sengoku/tmp/a.out)
==19008==  Address 0x4D5C040 is 16 bytes inside a block of size 20 alloc'd
==19008==    at 0x4A1B858: malloc (vg_replace_malloc.c:149)
==19008==    by 0x400574: main (in /home/sengoku/tmp/a.out)
==19008== 
==19008== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 8 from 1)
==19008== malloc/free: in use at exit: 0 bytes in 0 blocks.
==19008== malloc/free: 1 allocs, 1 frees, 20 bytes allocated.
==19008== For counts of detected errors, rerun with: -v
==19008== All heap blocks were freed -- no leaks are possible.

「Invalid read of size 8」、 すなわちアクセスすべきではないメモリを、 64bit (8 byte) 読み込み命令で読んだというエラー。

test.c で読み込みを行なう可能性があるところと言えば、 「bcopy(p->buf, buf, p->len);」の部分だけであり、 その範囲は printf で表示しているように、 0x4D5C034 番地から 0x4D5C043 番地までの 16 byte である。

ところが、Valgrind 曰く:

Address 0x4D5C040 is 16 bytes inside a block of size 20 alloc'd

ちょっと英語の意味が取りにくい (私の英語力が低いだけ? ^^;) が、 つまり「malloc で確保した 20 byte の領域のうち、 先頭から数えて 16 byte 目 (先頭は 0 byte 目と数える) が 0x4D5C040 番地であり、 この番地に対してメモリ読み込みが行なわれた」 という意味である (「16 byte 目」なら 「16 bytes」でなくて「16th byte」のような...?)。

すなわち、 「20 byte の領域のうち 16 byte 目」というのは残り 4 byte であり、 あと 4 byte コピーすればいいのにもかかわらず、 64bit 読み込み命令を使って 8 byte いっぺんに読んでしまっているから、 malloc で確保した領域の外をアクセスしてしまう、というわけ。

結果として 4 byte 無駄に読んでしまっている (実はコピー開始位置も 4 byte 前から行なうので、計 8 byte 余計に読み込んでいる) わけだが、 CPU にとって一番高速にコピーできる単位が (64bit 境界に合わせた) 64bit 読み書きだから、 bcopy の実装がこのようになっているのだろう。

より正確に言えば、 bcopy は 16 byte 以上のコピーを行なう場合は コピー開始位置手前の 64bit 境界 (alignment) の番地から 64bit ずつコピーし、 16 byte 未満の場合は byte 単位でコピーする。 test.c では、 コピー開始位置 p->buf が (直前のメンバが int32_t なので) 64bit 境界に一致しておらず、 しかもコピーする byte 数 p->len が 16 byte (= 64bit の倍数) なので、 16 byte 以上のコピーかつコピー終了位置も 64bit 境界に一致していない、 というのがミソである。

したがって 32bit な x86 Linux の場合であれば 32bit 単位でコピーを行なうので、 test.c ではこのようなエラーは起きない。 もちろん、64bit な x86_64 Linux で Valgrind がエラーを出すからといって、 bcopy の x86_64 における実装に問題がある、というわけではない。 Valgrind は、 あくまでバグの「可能性」を指摘するだけであって、 malloc で確保した領域の外へのアクセスでも、 それが意図的なものであれば (メモリ保護違反などでない限り) 何の問題もない。

分かってみれば単純な話なのであるが、 Valgrind のメッセージ「16 bytes inside a block」の意味が把握できなかった私は、 glibc の bcopy のソースを読んで 64bit 単位でコピーを行なっていることを知り、 4 byte の領域外読み込みが行なわれることを理解して初めて、 Valgrind のメッセージの意味が分かったという、 本末転倒な体験をした (^^;)。

ちなみに、 もちろん最初から上記のようなテストプログラムを Valgrind で チェックしようと思ったわけではなく、 「struct test」構造体は実際には次のような SockAddr 構造体であり、 saDup 関数にて malloc した SockAddr 構造体を doconnect 関数で bcopy する処理になっていて、 元ネタは拙作 stone である。

typedef struct {
    socklen_t len;
    struct sockaddr addr;
} SockAddr;
#define SockAddrBaseSize	((int)&((SockAddr*)NULL)->addr)
...

SockAddr *saDup(struct sockaddr *sa, socklen_t salen) {
    SockAddr *ret = malloc(SockAddrBaseSize + salen);
...

int doconnect(Pair *p1, struct sockaddr *sa, socklen_t salen) {
    struct sockaddr_storage ss;
    struct sockaddr *dst = (struct sockaddr*)&ss;	/* destination */
...
    bcopy(sa, dst, salen);
...

stone ML にて、 Valgrind で検証したらエラーが出た、という報告を頂いて (_O_) 以上のような調査を行なった次第。 bcopy に与えた引数に問題はなく、 どうしてこれが 「Invalid read of size 8」 エラーを引き起こすのか謎だった。 結果的には stone には問題はなく、 修正の必要もないことが判明したわけであるが、 今まで使っていなかった Valgrind を使ってみるいいきっかけになった。 実を言うと 64bit Linux を (プログラミングのレベルで) 使ったのも、 今回が初めてだったりする (^^;)。



このエントリーを含むブックマーク 2007年11月21日

FON ソーシャルルータ La Fonera のファームウェアは Linux であり、 RedBoot ブートローダ から起動される。 デフォルト状態の La Fonera では、 以下のように RedBoot がフラッシュメモリから Linux カーネルをロードして 自動起動する設定になっている。

== Executing boot script in 1.000 seconds - enter ^C to abort
RedBoot> fis load -l vmlinux.bin.l7
Image loaded from 0x80041000-0x801ba000
RedBoot> exec
Now booting linux kernel:
 Base address 0x80030000 Entry 0x80041000
 Cmdline : 
(以下略)

fis (Flash Image System) は、 フラッシュメモリの読み書きを行なうためのコマンドである。

fis load -l vmlinux.bin.l7」を実行することにより、 フラッシュメモリ内の「vmlinux.bin.l7」と名前をつけられた区画の内容を RAM へコピー (つまり load) する。

ところが、 fis load コマンドのマニュアルには、 「-l」オプションの記述がない。

Synopsis
fis load [-b load address] [-c ] [-d ] [name]

にもかかわらず、 La Fonera の RedBoot で fis コマンドのヘルプを表示させると、 「-l」オプションが指定できることが分かる。

RedBoot> fis help
*** invalid 'fis' command: unrecognized command
Usage:
  fis create -b <mem_base> -l <image_length> [-s <data_length>]
      [-f <flash_addr>] [-e <entry_point>] [-r <ram_addr>] [-n] <name>
  fis delete name
  fis erase -f <flash_addr> -l <length>
  fis free
  fis init [-f]
  fis list [-c] [-d]
  fis load [-d] [-l] [-b <memory_load_address>] [-c] name    ← コレ
  fis write -f <flash_addr> -b <mem_base> -l <image_length>

一体この「-l」オプションとは何なのか?

というか、 そもそも「vmlinux.bin.l7」という名前の見慣れない拡張子「L7」とは何なのか?
(L7 というと Layer 7 くらいしか思いつかない... ^^;)

というわけで調べてみた。

「vmlinux.bin.l7」の内容と、 「fis load -l」コマンドによって 0x80041000 番地にロードされた内容を見比べると、 「vmlinux.bin.l7」はデータ圧縮を行なった形式であるように見える。 おそらく「-l」オプションは、 フラッシュメモリ上の圧縮データを展開して RAM へロードするための オプションなのだろう。 圧縮データを展開するオプションとして「fis load」コマンドには、 すでに「-d」(gzip 圧縮データを展開) があるにもかかわらず、 何故わざわざ gzip 以外の圧縮形式を使っているのか謎であるが、 おそらくはより圧縮率の高い圧縮形式を使いたかったのだろう。

gzip より圧縮率が高い圧縮形式で「7」といえば 「7-Zip」、 という (安直な ^^;) 連想をもとに、 とりあえず手元にあった lzma コマンドで展開を試みてみる。

% lzma d vmlinux.bin.l7 vmlinux.bin

LZMA 4.43 Copyright (c) 1999-2006 Igor Pavlov  2006-06-04

ありゃ、あっさり展開できてしまった。 ちょっと拍子抜け。

展開した vmlinux.bin を TFTP サーバに置いて、 La Fonera に読み込ませて起動してみる:

RedBoot> load -r -b 0x80041000 vmlinux.bin
Using default protocol (TFTP)
Raw file loaded 0x80041000-0x8029aa37, assumed entry at 0x80041000
RedBoot> exec
Now booting linux kernel:
 Base address 0x80030000 Entry 0x80041000
 Cmdline :
(以下略)

これで、TFTP サーバに置いた任意のカーネルを La Fonera で起動することが できるようになった。 フラッシュメモリに書込む必要がないので手軽にカーネルの入れ替えができる。

ちなみに、 「vmlinux.bin」は RAM 上にコピーして即実行 (RedBoot の exec コマンド) 可能であることから、 raw binary 形式 (つまりオブジェクトファイルから、 シンボル情報やリロケーション情報を取り除いたもの) であることが分かる。 つまり、「vmlinux.bin」の拡張子「bin」は「raw binary」の意味なのだろう。 vmlinux から raw binary 形式のファイル vmlinux.bin を得るには、 次のように objcopy コマンドを実行すればよい。

% mips-linux-uclibc-objcopy -O binary vmlinux vmlinux.bin


hiroaki_sengoku at 06:49|この記事のURLComments(0)TrackBack(0)La Fonera