【Linux】【book】Binary Hacks の学習記録【SW】

下記の「Binary Hacks ―ハッカー秘伝のテクニック100選」を使った学習記録のページです。

10年ほど前に学習しローカルに保存しておいた内容を、知識の整理を兼ねて WEB 上に残していく。*1
特に、32bitマシンから 64bitマシンに変わったことで動作しない Tips が幾つかあった。

 


1章 イントロダクション
 1 Binary Hack入門
 2 Binary Hack用語の基礎知識
 3 fileでファイルの種類をチェックする
 4 odでバイナリファイルをダンプする

2章 オブジェクトファイルHack
 5 ELF入門
 6 静的ライブラリと共有ライブラリ
 7 lddで共有ライブラリの依存関係をチェックする
 8 readelfでELFファイルの情報を表示する
 9 objdumpでオブジェクトファイルをダンプする
 10 objdumpでオブジェクトファイルを逆アセンブルする
 11 objcopyで実行ファイルにデータを埋め込む
 12 nmでオブジェクトファイルに含まれるシンボルをチェックする
 13 stringsでバイナリファイルから文字列を抽出する
 14 c++filtでC++のシンボルをデマングルする
 15 addr2lineでアドレスからファイル名と行番号を取得する
 16 stripでオブジェクトファイルからシンボルを削除する
 17 arで静的ライブラリを操作する
 18 CとC++のプログラムをリンクするときの注意点
 19 リンク時のシンボルの衝突に注意する
 20 GNU/Linuxの共有ライブラリを作るときPICでコンパイルするのはなぜか
 21 statifierで動的リンクの実行ファイルを擬似的に静的リンクにする

3章 GNUプログラミングHack
 22 GCCのGNU拡張入門
 23 GCCでインラインアセンブラを使う
 24 GCCのビルトイン関数による最適化を活用する
 25 glibcを使わないでHello Worldを書く
 26 TLS(スレッドローカルストレージ)を使う
 27 glibcでロードするライブラリをシステムに応じて切り替える
 28 リンクされているライブラリによってプログラムの動作を変える
 29 ライブラリの外に公開するシンボルを制限する
 30 ライブラリの外に公開するシンボルにバージョンをつけて動作を制御する
 31 main()の前に関数を呼ぶ
 32 GCCが生成したコードによる実行時コード生成
 33 スタックに置かれたコードの実行を許可/禁止する
 34 ヒープ上に置いたコードを実行する
 35 PIE(位置独立実行形式)を作成する
 36 C++でsynchronized methodを書く
 37 C++でシングルトンを生成する
 38 g++の例外処理を理解する(throw編)
 39 g++の例外処理を理解する(SjLj編)
 40 g++の例外処理を理解する(DWARF2編)
 41 g++ 例外処理のコストを理解する

4章 セキュアプログラミングHack
 42 GCCセキュアプログラミング入門
 43 -ftrapvで整数演算のオーバーフローを検出する
 44 Mudflap でバッファオーバーフローを検出する
 45 -D_FORTIFY_SOURCEでバッファオーバーフローを検出する
 46 -fstack-protectorでスタックを保護する
 47 bitmaskする定数は符号なしにする
 48 大きすぎるシフトに注意
 49 64ビット環境で0とNULLの違いに気を付ける
 50 POSIXのスレッドセーフな関数
 51 シグナルハンドラを安全に書く方法
 52 sigwaitで非同期シグナルを同期的に処理する
 53 sigsafeでシグナル処理を安全にする
 54 Valgrindでメモリリークを検出する
 55 Valgrindでメモリの不正アクセスを検出する
 56 Helgrindでマルチスレッドプログラムのバグを検出する
 57 fakerootで擬似的なroot権限でプロセスを実行する

5章 ランタイムHack
 58 プログラムがmain()にたどりつくまで
 59 システムコールはどのように呼び出されるか
 60 LD_PRELOADで共有ライブラリを差し換える
 61 LD_PRELOAD で既存の関数をラップする
 62 dlopenで実行時に動的リンクする
 63 Cでバックトレースを表示する
 64 実行中のプロセスのパス名をチェックする
 65 ロードしている共有ライブラリをチェックする
 66 プロセスや動的ライブラリがマップされているメモリを把握する
 67 libbfdでシンボルの一覧を取得する
 68 C++ のシンボルを実行時にデマングルする
 69 ffcallでシグネチャを動的に決めて関数を呼ぶ
 70 libdwarfでデバッグ情報を取得する
 71 dumperで構造体のデータを見やすくダンプする
 72 オブジェクトファイルを自力でロードする
 73 libunwindでコールチェインを制御する
 74 GNU lightningでポータブルに実行時コード生成する
 75 スタック領域のアドレスを取得する
 76 sigaltstackでスタックオーバーフローに対処する
 77 関数へのenter/exitをフックする
 78 シグナルハンドラからプログラムの文脈を書き換える
 79 プログラムカウンタの値を取得する
 80 自己書き換えでプログラムの動作を変える
 81 SIGSEGVを使ってアドレスの有効性を確認する
 82 straceでシステムコールをトレースする
 83 ltraceで共有ライブラリの関数呼び出しをトレースする
 84 JockeyでLinuxのプログラムの実行を記録、再生する
 85 prelinkでプログラムの起動を高速化する
 86 livepatchで実行中のプロセスにパッチをあてる

6章 プロファイラ・デバッガHack
 87 gprofでプロファイルを調べる
 88 sysprofでお手軽にシステムプロファイルを調べる
 89 oprofileで詳細なシステムプロファイルを得る
 90 GDBで実行中のプロセスを操る
 91 ハードウェアのデバッグ機能を使う
 92 Cのプログラムの中でブレークポイントを設定する

7章 その他のHack
 93 Boehm GCの仕組み
 94 プロセッサのメモリオーダリングに注意
 95 Portable Coroutine Library(PCL)で軽量な並行処理を行う
 96 CPUのクロック数をカウントする
 97 浮動小数点数のビット列表現
 98 x86が持つ浮動小数点演算命令の特殊性
 99 結果が無限大やNaNになる演算でシグナルを発生させる
 100 文献案内

 
 

4 odでバイナリファイルをダンプする

 
16進数表記で1バイトずつ(x1)表示する。オフセットは16進数表記(-Ax)

% od -t x1 -Ax $target | head -n 5

 
さらに ASCII 文字表示も加える(-t に z を付加)

% od -t x1z -Ax $target | head -n 5

 
-v でダンプを省略しない(デフォルトでは同一行が続く場合,省略する)

% od -tx1 -Ax -v $target | sed -n '45,49p'

 
--strings で文字列のダンプ機能(stringsコマンドを使う方が良い)

% od -Ax --strings $target | head -n 10

 
特定の位置(例:0x34バイト目)から表示

% od -t x1z -Ax -j 0x0034 $target | head -n1

 
 

7 lddで共有ライブラリの依存関係をチェックする

 
LINUX では,ldd は単なるスクリプトになっており,実体は内部で LD_TRACE_LOADED_OBJECTS=1 としている。

tcshの場合

% env LD_TRACE_LOADED_OBJECTS=1 ls  # env 必要
  linux-gate.so.1 =>  (0x40000000)
  librt.so.1 => /lib/librt.so.1 (0x002e3000)
  libselinux.so.1 => /lib/libselinux.so.1 (0x002f5000)
  libacl.so.1 => /lib/libacl.so.1 (0x00ddc000)
  libc.so.6 => /lib/libc.so.6 (0x00101000)
  libpthread.so.0 => /lib/libpthread.so.0 (0x00dc1000)
  /lib/ld-linux.so.2 (0x002c4000)
  libdl.so.2 => /lib/libdl.so.2 (0x00dba000)
  libattr.so.1 => /lib/libattr.so.1 (0x0072d000)

bashの場合

$ LD_TRACE_LOADED_OBJECTS=1 ls      # env 不要

 

■ 共有ライブラリの依存関係

objdump -p より,Dynamicセクションの NEEDED に記録されていることが分かる。

% objdump -p /bin/ls

./ls:     file format elf32-i386

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000100 memsz 0x00000100 flags r-x
  INTERP off    0x00000134 vaddr 0x08048134 paddr 0x08048134 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
(中略)
Dynamic Section:
  NEEDED      librt.so.1
  NEEDED      libselinux.so.1
  NEEDED      libacl.so.1
  NEEDED      libc.so.6
(以下略)

 
また,readelf -d でも確認出来る。

% objdump -d /bin/ls

Dynamic section at offset 0x17560 contains 27 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [librt.so.1]
 0x00000001 (NEEDED)                     Shared library: [libselinux.so.1]
 0x00000001 (NEEDED)                     Shared library: [libacl.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
(以下略)

このように /bin/ls は,librt.so.1, libacl.so.1, libc.so.6, libselinux.so.1 の 4つの共有ライブラリを必要としていることが分かる。

しかし,/bin/ls を実行する場合に必要なのは,この3つの共有ライブラリだけではない。
この3つの共有ライブラリ自体がそれぞれ必要としている別の共有ライブラリも必要となる。
つまり NEEDED で記録されているものは SONAME のため,SONAME から実際のファイルを探してくる必要がある。

 
 

8 readelf で ELF ファイルの情報を表示する

readelf は BFD ライブラリを使わずに直接 ELF ファイルを読むためのツール。
BFD に依存しないことから,ELF ファイルの問題か BFD ファイルの問題かの切り分けがし易くなる。

■ ELFヘッダの読み出し

見たいヘッダ オプション ロングオプション
ELFファイルヘッダ -h -file-header
プログラムヘッダ -l -program-headers, --segments
セクションヘッダ -S -section-headers, --sections
以上の3つのヘッダ -e -headers

 
■ ELF情報の読み出し

見たいヘッダ オプション ロングオプション
シンボルテーブル -s --syms, -symbols
リロケーション情報 -r --relocs
ダイナミックセグメント -d --dynamic
バージョンセクション -V --version-info
アーキテクチャ依存 -A --arch-specific
パケットリスト長のヒストグラム -I --histogram
ヘッダすべてと以上のすべて -a --all
コアノート(core notes) -n --notes
unwind情報 -u --unwind

 
通常,シンボル情報はシンボルセクションにあるシンボル情報を使うが,-Dオプション(--use-dynamic)を使うと,ダイナミックセクションにあるシンボル情報として,シンボル情報を使うようになる.

■ ELFセクションのダンプ

  • xオプション (--hex-dump) で指定したセクションの内容をダンプする。

セクションはセクション番号で指示する。
セクション番号は -S オプションで表示されるセクションヘッダに付いている番号である。

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048168 000168 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0804818c 00018c 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481ac 0001ac 000060 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804820c 00020c 000053 00   A  0   0  1

 
.interp はセクション番号が 1 なので,その内容を見るためには次のようにする.

% readelf -x1 a.out
Hex dump of section '.interp':
  0x08048134 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
  0x08048144 2e3200                              .2.

 
.note.ABI-tag であれば 2,.dynsym であればセクション番号は 3 となる

 
■ DWARF2デバッグセクションの読み出し

  • wオプション(--debug-dumpオプション)でDWARF2デバッグセクションの情報を表示する。
-w --debug-dump= セクション
l line .debug_line
i info .debug_info
a abbrev .debug_abbrev
p pubnames .debug_pubnames
r aranges .debug_aranges
R Ranges .debug_ranges
m macro .debug_macinfo
f frames .debug_frame
F frames-interp .debug_frame
s str .debug_str
o loc .debug_loc

 
■ 長いシンボルもすべて表示する

  • W(--wide)を使うと80文字以上の長い出力も可能になる。

 
 

9 objdump でオブジェクトファイルをダンプする

10 objdump でオブジェクトファイルを逆アセンブルする

 
objdump

 オブジェクトファイルの情報を表示する。
 objdump は x86 のような LSB環境(最下位バイト)においても,
 MSB環境(最上位バイト)のように表示される

 
全表示させる(-s/--full-contents)

objdump -s $target

 
ELFバイナリの特定セクションのみダンプする(-s -j)

objdump -s -j .interp $target

 
どのようなセクションがあるか表示する(-h)

objdump -h $target

 
アドレス範囲を指定してダンプする(--start-address/--stop-address)

objdump -s --start-address=0x08000000 --stop-address=0x08ffffff $target

 
単なるバイナリとして表示する(odと同じ)
(binaryフォーマットは自動認識されないので必ず指定する)

objdump -s -b binary $target

 
利用可能なフォーマットを表示する(-i)

objdump -i $target

 
テキスト領域のみ逆アセンブルする(-d/--disassemble)

objdump -d $target

 
アドレスをシンボルからのオフセットで表示する(--prefix-address)

objdump -d --prefix-address $target
objdump -d --prefix-address --show-raw-insn $target  #コードのバイト列表示

 
全領域を逆アセンブルする(-D/--disassemble-all)
(通常はテキスト領域のみが対象)

objdump -D $target

 
セクションとアドレス範囲を指定して,逆アセンブルする

objdump -d -j .init $target     # .initセクションを対象としている

 
ソースファイルとの対応を表示する(-l/--line-numbers)

objdump -d -l $target

 
★ ソースファイルとの対応 + ソースコードを挿入してくれる(-S/--source)

objdump -d -S $target

 
 
 

13 stringsでバイナリファイルから文字列を抽出する

  • tx オプションを付けると 16進数で文字列が現れる位置を表示してくれる。
  • td だと 10進数,-to だと 8進数表示になる。
% strings -tx ./a.out

    134 /lib/ld-linux.so.2
    17d Kf,M
    1fd __gmon_start__
    20c libc.so.6
    216 _IO_stdin_used
    225 puts
    22a __libc_start_main
    23c GLIBC_2.0
    2f8 PTRh0
    4a4 [^_]
    500 AAAAAAAAA
    50a BBBBBBBBB
    514 CCCCCCCCC
    510 #### finish ####
    54a #### start ####

オブジェクトファイルの場合は,テキストセクションのみを文字列抽出の対象としてしまう。
そこで -a オプションを付けることで,データセクションも検索対象とすることが出来る。

% strings -a -tx a.out

 
 

25 glibcを使わないでHello Worldを書く

glibc を使わないで Hello World を書く

サンプルとして,単に retrun するだけのプログラム(no_glibc1.c)も用意した。
(Wataru's memo(2006-08-26) より拝借)

glibc を使わないで Hello World を書くプログラムは no_glibc2.c とした.

no_glibc1.c

/*
 * glibc を使わないでプログラムを作成し動かす.
 * http://memo.wnishida.com/?date=20060826 より 
 *
 * 通常,glibc を初期化するための crt*.o がリンクされ,
 * これによって実行ファイルのサイズが大きくなる.
 * そこで,glibc をリンクしないようにする.
 *
 * コンパイル&リンクは次の通り
 * % gcc -Wall -fno-builtin -c no_glibc1.c
 * % ld no_glibc1.o
 *
 * 実行すると終了値(99)が返される
 * % ./a.out ; echo $?
 * 99
 */
#include <sys/syscall.h>
static int errno;
void exit(int status);

void
_start()
{
  int  i = 99;
  exit(i);    /*  exitシステムコールが必要. return i; だと SEGV */
}

void
exit(int status)
{
  long __res;

  asm volatile
  (
    "int $0x80": /* テンプレート */
    "=a"(__res)  : /* 出力OPR */
    "0"(__NR_exit), "b"((long)status) : /* 入力OPR */
    "memory"
  );

  do
  {
    if((unsigned long)(__res) >= (unsigned long)-127)
    {
      errno = -(__res);
      __res = -1;
    }
    return (void)__res; /* -fno-builtin でwarningを消す */
  }
  while(0);
};

 
no_glibc2.c

/*
 * glibc を使わないで Hello World を書く.
 * http://memo.wnishida.com/?date=20060826 より 
 *
 * 通常,glibc を初期化するための crt*.o がリンクされ,
 * これによって実行ファイルのサイズが大きくなる.
 * そこで,glibc をリンクしないようにする.
 *
 * コンパイル&リンクは次の通り
 * % gcc -Os -fno-builtin -fomit-frame-pointer -fno-ident -c no_glibc2.c
 * % ld --entry=hello -o hello no_glibc2.o   # エントリーを hello() と設定
 */

//#include <sys/syscall.h> /* こっちでも良さそう */
#include <asm/unistd.h>

static int errno;
void exit(int status);

void
hello()
{
  write(1, "Hello World!\n", 13);
  exit(0);    /*  exitシステムコールが必要. return 0; だと SEGV */
}


int
write(int fd, const void * buf, unsigned long count)
{
  long  __res;

  asm volatile
  (
    "int $0x80"
    : "=a" (__res)
    : "0"  (__NR_write), "b"((long)fd), "c"((long)buf), "d"((long)count)
    : "memory"
  );

  if((unsigned long)(__res) >= (unsigned long)-127)
  {
    errno = -(__res);
    __res = -1;
  }
  return (int)__res;
}


void
exit(int status)
{
  long __res;

  asm volatile
  (
    "int $0x80": /* テンプレート */
    "=a"(__res)  : /* 出力OPR */
    "0"(__NR_exit), "b"((long)status) : /* 入力OPR */
    "memory"
  );

  do
  {
    if((unsigned long)(__res) >= (unsigned long)-127)
    {
      errno = -(__res);
      __res = -1;
    }
    return (void)__res; /* -fno-builtin でwarningを消す */
  }
  while(0);
};

 
 

54 Valgrind でメモリリークを検出する

 
Linux/x86 ,Linux/amd64 ,Linux/ppc32 に対応したプログラムの動作を動的に解析してくれるツール.
以下のチェックをしてくれる.

  • メモリリーク
  • 不正リリース
  • 二重解放
  • 不正アクセス
  • マルチスレッドでのメモリアクセスの競合

gcc -g オプションを付け,-O1までに最適化を抑えておく。また staticリンクも避ける.

% valgrind --leak-check=full --leak-resolution=high --show-reachable=yes ./a.out

 
valgrind1.c
プログラムが終了するとメモリが解放されるので,意図的に free() を使っていないパターン.
 
valgrind2.c
配列の範囲外にアクセスするパターン。配列は BSS, Stack 領域の2つを使っている。

 
valgrind1.c

/* Valgrind のテストプログラム */
/* alloca.c を題材にしてみたが,2ヶ所解放漏れがあった。
 * #54 で検索すると該当個所が見つかる */

/* alloca -- (mostly) portable public-domain implementation -- D A Gwyn

   last edit:   86/05/30        rms
   include config.h, since on VMS it renames some symbols.
   Use xmalloc instead of malloc.

   This implementation of the PWB library alloca() function,
   which is used to allocate space off the run-time stack so
   that it is automatically reclaimed upon procedure exit, 
   was inspired by discussions with J. Q. Johnson of Cornell.

   It should work under any C implementation that uses an
   actual procedure stack (as opposed to a linked list of
   frames).  There are some preprocessor constants that can
   be defined when compiling for your specific system, for
   improved efficiency; however, the defaults should be okay.

   The general concept of this implementation is to keep
   track of all alloca()-allocated blocks, and reclaim any
   that are found to be deeper in the stack than the current
   invocation.  This heuristic does not reclaim storage as
   soon as it becomes invalid, but it will do so eventually.

   As a special case, alloca(0) reclaims storage without
   allocating any.  It is a good idea to use alloca(0) in
   your main control loop, etc. to force garbage collection.
*/
/*=========*/
/* include */
/*=========*/
#include <stdio.h>
#include <stdlib.h>

/*========*/
/* define */
/*========*/
#define DEBUG
#if defined(DEBUG)
#define DBP printf
#else
#define DBP 1 ? (void)0 : (void)
#endif
#define xmalloc malloc
#define xfree   free
/*
  スタックの伸長方向
  STACK_DIRECTION > 0 ...高アドレス方向へ伸びる
  STACK_DIRECTION < 0 ...低アドレス方向へ伸びる 
  STACK_DIRECTION = 0 ...伸長報告が不明な状態
*/
#ifndef STACK_DIRECTION /* スタックの伸長方向(高位/低位アドレス)が不明な場合 */
#define STACK_DIRECTION    0   
#endif

#if STACK_DIRECTION != 0 /* 予めスタックの伸長方向が分かっている場合 */
#define    STACK_DIR    STACK_DIRECTION   /* コンパイル時に定義する */
#endif /* STACK_DIRECTION */

#define STACK_DIR    stack_dir

#ifndef    ALIGN_SIZE
#define    ALIGN_SIZE    sizeof(double)
#endif

/*========*/
/* struct */
/*========*/
typedef char    *pointer;        /* generic pointer type */

/* mallocしたときに付加するヘッダ情報 */
typedef union hdr
{
  char    align[ALIGN_SIZE];/* ヘッダサイズ調整 */
  struct
  {
    union hdr *next; /* 同一関数で malloc された領域へのポインタ */
    char *deep;      /* mallocしたときのスタック領域のアドレス   */
  } h;
} header;

/*=================*/
/* static variable */
/*=================*/
static int    stack_dir;        /* 1 or -1 once known */
static header *St_p_last = NULL; /* mallocした最後のノード */

/*==========*/
/* function */
/*==========*/
void * myalloca (unsigned size);
void   sub(void);
static void check_stack_direction (void);

/*===============*/
/* routine start */
/*===============*/
/* スタックが低位アドレスに伸びるのか、* 
 * 高位アドレスに伸びるのかチェック    */
static void
check_stack_direction(void)
{
  static char   *addr = NULL;
  auto   char    dummy;   /* 適当なローカル変数を作成 */

  if (addr == NULL) /* 初回 */
  {                
    addr = &dummy;
    check_stack_direction ();    /* recurse once */
  }
  else             /* 2回目以降 */
  {
    stack_dir = (&dummy > addr) ? 1 : -1; /* スタックの伸長方向チェック */
    /* -1...低アドレス方向へ伸びる , 1...高アドレス方向へ伸びる */
  }
}

void *
myalloca(unsigned size)
{
  auto     char    probe; /* myalloca()呼出し時のスタック領域の先端のために使用 */
  register char    *depth = &probe; 

#if STACK_DIRECTION == 0
  if (STACK_DIR == 0) /* 0...スタックの伸長報告が不明な場合 */ 
  {                  
    check_stack_direction (); /* スタックの伸長報告を確認 */
  }                                
#endif
  register header  *hp;   /* リンクリストを辿るときに使用 */

  for (hp = St_p_last; hp != NULL;)
  {
    DBP("(old, new) = (%p, %p)\n", hp->h.deep, depth);
    /* スタック領域が不使用になっていた場合は free()する */
    if
    (  
      ((STACK_DIR > 0) && (hp->h.deep > depth))/* grew upwardのとき, */
     || ((STACK_DIR < 0) && (hp->h.deep < depth))/* grew downwardのとき */
    )
    {
      register header    *np = hp->h.next;
      DBP("FREE\n");
      xfree ((pointer) hp);
      hp = np;
    }
    else
    {
      break;  /* rest are not deeper */
    }
  }
  /* mallocで取得した領域(仮想スタックの先頭)を記憶しておく */ 
  St_p_last = hp;

  if (size == 0)
  { 
    return NULL;  /* no myallocation required */
  }

  /* ヘッダ領域を付加しておく. mallocをxmallocとしてdefine済み */
  register pointer p_new = xmalloc (sizeof (header) + size);      /* メモリリーク(1) #54 */

  ((header *)p_new)->h.next = St_p_last;
  ((header *)p_new)->h.deep = depth; /* ここでスタックアドレスを記憶させておく */

  /* free()のためにmallocで取得した領域を記憶しておく */ 
  St_p_last = (header *)p_new;

  return (pointer)((char *)p_new + sizeof(header)); /* ヘッダ領域を返さないように */
}


/* 以下、テスト関数 */
int
main()
{
  char *p = NULL;

  sub();

  /* sub()で alloca() した分を free() した後に alloca する */
  /* sub()内で2度 alloca していた場合は、2度 free される */
  p = myalloca(256);  /* メモリリーク(2) #54 */

  return 0;
}

void
sub(void)
{
  char *p = NULL;

  p = myalloca(1);
  p = myalloca(1); /* 関数中なのでスタックにPUSHする */
}

 
valgrind2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static char onbss[128];

int
main()
{
  char   onstack[128];
  int    uninitialized;
  int    dummy;
  char * onheap = (char *)malloc(128);

  dummy = onbss[128];   /* 配列オーバー *//* Data領域の不正アクセス */
  dummy = onstack[150]; /* 配列オーバー *//* BSS領域の不正アクセス */

  if(uninitialized == 0)
  { 
    printf("hello world\n");
  }
  close(uninitialized);

  dummy = onheap[128];

  free(onheap);
  dummy = onheap[0]; /* 解放したのにアクセスしている */

  strcpy(onstack, "build one to throw away; you will anyway.");
  strcpy(onstack, onstack + 1);   /* 転送元と先が重なっている */

  return 0;
}

 
 

59 システムコールはどのようにして呼び出されるのか

 
int 0x80 意外の方法でシステムコールを呼び出すには sysenter を使う。
ただし,sysenter は kernel2.6 からサポートされたので注意。
sysenter では複数のシステムコールをサポートする vsyscall というメカニズムに変更された。

下記の結果より,0xffffe400 <__kernel_vsyscall> からシステムコール呼び出しを行っていることが判明したので,インラインアセンブラを使って sysenter を呼び出す。

% cat /proc/self/maps | grep vdso
ffffe000-fffff000 ---p 00000000 00:00 0    [vdso]
% dd if=/proc/self/mem of=vdso bs=1 skip=0xffffe000 count=4096
% readelf -S vdso | grep -e ' Name ' -e '.text '
[Nr] Name     Type       Addr     Off    Size   ES  Flg  Lk  Inf Al
[ 6] .text    PROGBITS   ffffe400 000400 000060 00  AX   0   0   32
% objdump -d vdso --start-address=0xffffe400
...
ffffe400 <--kernel_vsyscall>:
ffffe400:       51               push   %ecx
ffffe401:       52               push   %edx
ffffe402:       55               push   %ebp
ffffe403:       89 e5            mov    %esp,%ebp
ffffe405:       0f 34            sysenter
ffffe407:       90               nop

 

sample1.c

#include <stdio.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int
main()
{
  int  ret;

  ret = syscall(__NR_getpid); /* __NR_getpid = 20 */
  printf("ret = %d pid = %d\n", ret, getpid());

  return 0;
}


 
sample2.c

#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

int
main()
{
  int  ret;
  asm volatile
  (
    "int $0x80": /* テンプレート */
    "=a"(ret)  : /* 出力OPR:
            retを %eaxに割当て, asm終了後に retに __NR_getpidを代入する。
            最初のレジスタなので,テンプレートから %0 で参照可,また
            オペランドからは "0" で参照可 */
    "0"(__NR_getpid)  /* 入力OPR: __NR_getpid を "0" つまり %eax へ代入する */
  );

  printf("ret = %d pid = %d\n", ret, getpid());

  return 0;
}

 
sample3.c

#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <linux/version.h>

int
main()
{
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,6,0) /* 多分これで良いはず */
  int pid = 0;

  asm volatile
  (
    "call *%2 \n": /* %2は "S"である %esiのことで,__kernel_vsyscallを呼ぶ */
    "=a"(pid)    : /* 出力OPR: pidを %eaxに割当て, asm終了後に pid に __NR_getpidを代入 */
    "0"(__NR_getpid), /* 入力OPR: __NR_getpid を "0" つまり %eax へ代入する */
    "S"(0xffffe400)   /* 入力OPR: %esiに 0xfffffe400 を代入する */
  );

  printf("pid = %d pid = %d\n", pid, getpid());

#endif /* LINUX_VERSION_CODE > KERNEL_VERSION(2,5,2) */
  return 0;
}

 
 

62 dlopenで実行時に動的リンクする

次の挙動をするプログラム

1. dlopen.c の実行ファイル(dlsay)が実行時に hello.so をロードする
2. hello.so が提供する echo_hello(char *) を呼び出す
3. 2 のときに引数も渡す

dlopen1.c

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

void
invoke_dlopen(int argc, char * argv[])
{
    void * handle = NULL;
    char * error  = NULL;
    char *(*msg)();

    /* RTLD_LAZY ... ロード時にシンボルの値の解決をさぼる(LAZY) */
    handle = dlopen(argv[1], RTLD_LAZY);
    if(handle == NULL)
    {
        fprintf(stderr, "load error %s: %s\n", argv[1], dlerror());
        exit(1);
    }

    dlerror(); /* clear error */
    msg = (char *(*)()) dlsym(handle, argv[2]); /* 呼び出し側でキャストが必要 */
    error = dlerror();
    if(error)
    {
        fprintf(stderr, "dlsym error %s: %s\n", argv[2], error);
        exit(2);
    }

    printf("%s\n", (*msg)(argc > 3 ? argv[3] : NULL));
    dlclose(handle);

    exit(0);
}

int
main(int argc, char * argv[])
{
    invoke_dlopen(argc, argv);

    return 0;
}

 
hello.c

#include <stdio.h>

char *
echo_hello(char * arg)
{
    static char buf[4096];

    snprintf(buf, sizeof(buf), "Hello, %s", arg);

    return buf;
}

 
Makefile

all: dlopen1.c hello.c
	gcc -Wall -o dlsay dlopen1.c -ldl
	gcc -shared -fPIC -o hello.so hello.c
	./dlsay ./hello.so echo_hello hoge

clean:
	rm -f *.o *.so *~ dlsay

RTLD_LAZY と RTLD_NOW

dlopen(3)で指定している RTLD_LAZY というのは,ロード時にシンボルの値の解決をさぼる(lazy)という意味。
RTLD_LAYZ の代わりに RTLD_NOW を指定すると,ロード時にシンボルの値の解決をする。
もしシンボルの値の解決に失敗すると,dlopen(3) 自体が失敗となる。
RTLD_LAZY の場合は解決できないシンボルがあっても dlsym(3) で調べるまで解決しないので,dlopen(3)はエラーとはならない。
なお,プログラムでは RTLD_LAZY となっていても,環境変数に LD_BIND_NOW に何か文字列を設定しておくと,RTLD_NOW と同じ動作をするようになる。

RTLD_LOCAL と RTLD_GLOBAL

RTLD_LAZY もしくは,RTLD_NOW に対して RTLD_LOCAL や RTLD_GLOBAL のどちらかを設定することが出来る。
RTLD_GLOBAL にすると dlopen(3) で読み込んだ共有オブジェクトにあるシンボルが他の共有オブジェクトでのシンボル解決にも自動的に使われるようになる。
デフォルトは RTLD_LOCAL で,dlopen(3) で読み込んだ共有オブジェクトのシンボルは他には影響しない。

dlopen(3) で返されたハンドルを使うことで,その共有オブジェクトに含まれるシンボルの値を得ることが出来る。
そのために使うのが,dlsym(3)で,dlsym に共有オブジェクトのハンドルとシンボル名を与えると,その共有オブジェクトの中で定義されているシンボルの値を返す。
シンボルが見つからない場合は NULL を返す。(エラーの内容は dlerror() で確認出来る)

dlsym(3) で取得出来るのはシンボルの値だけなので,そのシンボルがどのような変数,関数なのかは呼び出し側が適当にキャストする必要がある。

dlclose(3)を呼び出すと,そのハンドルに対応した共有オブジェクトのマッピングが外される。
同じ共有オブジェクトが複数 dlopen(3) されている時は,dlopen(3) された回数 dlclose(3) されるまで実際にはマッピングが残ったままになる。

 
 

65 ロードしている共有ライブラリをチェックする

■ ロードアドレス
共有ライブラリは実行直前にロードアドレスが決まる.
ロードアドレスは /proc/$$/maps あるいは /proc/self/maps で確認出来る.

% sleep 3600 &
[2] 3314
% cat /proc/3314/maps 
00110000-00111000 r-xp 00110000 00:00 0          [vdso]
00111000-00264000 r-xp 00000000 fd:00 17744557   /lib/libc-2.7.so
00264000-00266000 r-xp 00153000 fd:00 17744557   /lib/libc-2.7.so
00266000-00267000 rwxp 00155000 fd:00 17744557   /lib/libc-2.7.so
00267000-0026a000 rwxp 00267000 00:00 0 
002c4000-002df000 r-xp 00000000 fd:00 17744556   /lib/ld-2.7.so
002df000-002e0000 r-xp 0001a000 fd:00 17744556   /lib/ld-2.7.so
002e0000-002e1000 rwxp 0001b000 fd:00 17744556   /lib/ld-2.7.so
08048000-0804c000 r-xp 00000000 fd:00 9788163    /bin/sleep
0804c000-0804e000 rw-p 00003000 fd:00 9788163    /bin/sleep
08793000-087b4000 rw-p 08793000 00:00 0 
b7cb9000-b7da7000 r--p 03387000 fd:00 2294792
/usr/lib/locale/locale-archive
b7da7000-b7fa7000 r--p 00000000 fd:00 2294792
/usr/lib/locale/locale-archive
b7fa7000-b7fa8000 rw-p b7fa7000 00:00 0 
b7fbb000-b7fbc000 rw-p b7fbb000 00:00 0 
bfb67000-bfb7c000 rw-p bffea000 00:00 0          [stack]

ログの書式は次の順番である。
マップ位置,保護属性,ファイル内(共有ライブラリ)のオフセット,デバイスのメジャ:マイナ番号,i-node,ファイル名(共有ライブラリ)


上記の情報をダイナミックに取得するには,以下の2通りがある

(A) procfs をパースする
OS が procfs をサポートしている場合は,上記 /proc/$$/maps ファイルをパースすれば良い.
ただし,prelink によってロードアドレスが不要になるケースがあるので注意が必要である.

(B) dl_iterate_phdr(3) を使う
Linux では dl_iterate_phdr(3) という関数が glibc にあるので利用出来る.

dl_iterate_phdr.c

/* gcc dl_iterate_phdr.c -ldl */
#define _GNU_SOURCE
#include <stdio.h>
#include <link.h>

static
int print_callback
(
    struct dl_phdr_info * info,
    size_t                size,
    void                * data
)
{
    printf("%08x %s\n", info->dlpi_addr, info->dlpi_name);
    return 0;
}

int
main()
{
    dl_iterate_phdr(print_callback, NULL); /* 第二引数が print_callback() の引数になる */
    return 0;
}

実行してみる

% ./a.out
00000000                        <---- 自分自身のプロセス情報で dl_iterate_phdr では表示出来ない
00112000 
00000000 /lib/libdl.so.2        <---- prelink が実行されていて,すでにシンボルの値にロードアドレス分のオフセットが加わっている為に 0
00010000 /lib/libc.so.6
00000000 /lib/ld-linux.so.2

なお,FreeBSD/solaris では,dlinfo() を使って共有ライブラリ&ロードアドレス情報を取得する.


また,共有ライブラリ名と関数の名前が何か一つでも分かっていれば,dlopen, dlsym, dladdr を組み合わせてロードアドレスを調べることが出来る↓
dl_iterate.c

/* #65-2 dlopen(), dlsym(), dladdr() を使ってロードしている共有ライブラリをチェックする */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>   /* GNU拡張機能を予め有効にしておくこと(= _GNU_SOURCE) */
#include <string.h>

#if 0  /* Dl_info 型のメンバ */
    typedef struct
    {
         const char *dli_fname;  /* 共有オブジェクトのファイル名           */
         void *dli_fbase;        /* 共有オブジェクトのロードされたアドレス */
         const char *dli_sname;  /* シンボル名                             */
         void *dli_saddr;        /* シンボル値                             */
    }
        Dl_info;
#endif /* Dl_info 型のメンバ */

enum      { SUCCESS, FAILURE };
enum      { FALSE,   TRUE };

static int try_dl_iterate( char * lib, char * method);

int
main(int argc, char * argv[])
{
    int err      = SUCCESS;

    if(argc != 3)
    {
        fprintf
        (
            stderr,
            "Usage: %s [share_lib name] [symbol name in share_lib]\n",
            argv[0]
        );
        exit(99);
    }

    char * lib_name = argv[1];
    char * method   = argv[2];

    err = try_dl_iterate(lib_name, method);
    if(err)
    {
        fprintf(stderr, "Could not get share lib information\n");
        return FAILURE;
    }

    return SUCCESS;
}

static int
try_dl_iterate( char * lib, char * method)
{
    int       (*func)();
    void    * dl_handle = NULL;
    char    * error     = NULL;
    Dl_info * info      = malloc(sizeof(Dl_info));  /* 予め領域を確保しておくこと */
    int       is_exist  = FALSE;
    
    /* Open the shared object */
    dl_handle = dlopen(lib, RTLD_LAZY);
    if(dl_handle == NULL)
    {
        fprintf(stderr, "!!! %s\n", dlerror());
        return FAILURE;
    }
    
    /* Resolve the symbol (method) from the object */
    func  = dlsym(dl_handle, method);
    error = dlerror();
    if(error != NULL)
    {
        fprintf(stderr, "!!! %s\n", error );
        return FAILURE;
    }
    
    /* dladdr(void *, Dl_info *) なので,第二引数は予め領域を確保しておく */
    is_exist = dladdr((void *)func, info);
    error    = dlerror();
    if(!is_exist) 
    {
        fprintf(stderr, "!!! %s\n", error);
        return FAILURE;
    }

    printf("%08x %s\n", (unsigned)(info->dli_fbase), info->dli_fname);

    /* Close the object */
    dlclose(dl_handle);
    
    return SUCCESS;
}

 
 

66 プロセスや動的ライブラリがマップされているメモリを把握する

 
プロセスが起動すると,その実行バイナリファイルや動的ローダによってロードされた共有ライブラリファイルが仮想メモリ空間にマッピングされる。
(どのようなファイルがマッピングされているかは,#65 の /proc/$$/mapsを確認することで確認出来るが) 仮想メモリの範囲がどのようになっているかは, 下記のように pmap コマンドで確認することが出来る。

以下、sleep コマンドで pmap の挙動を確認する。

1. sleep コマンドを実行する

プロセスID hal 25619 である。

% /bin/sleep 10000 &
[4] 25619
% ps auxw | grep sleep
Neko  25619  0.0  0.0   4748   500 pts/1    S    03:13   0:00 /bin/sleep 10000
Neko  25621  0.0  0.1   5132   840 pts/1    S+   03:13   0:00 grep -i sleep

 
2. pmap を実行する

% pmap 25619

25619:   /bin/sleep 10000
00110000      4K r-x--    [ anon ]      ★ 4KBアライメント = ページサイズが4KBと判明
00111000   1356K r-x--  /lib/libc-2.7.so
00264000      8K r-x--  /lib/libc-2.7.so
00266000      4K rwx--  /lib/libc-2.7.so
00267000     12K rwx--    [ anon ]
002c4000    108K r-x--  /lib/ld-2.7.so
002df000      4K r-x--  /lib/ld-2.7.so
002e0000      4K rwx--  /lib/ld-2.7.so
08048000     16K r-x--  /bin/sleep      ★ 下記 3 の .interp 〜 .plt セクションに該当する(読み+実行専用)
0804c000      8K rw---  /bin/sleep      ★ 下記 3 の .text 〜 に該当する(読み+書き領域(多分グローバル変数領域))
08262000    132K rw---    [ anon ]      ★ 恐らく,Heap領域であり,高位アドレスに向かって伸びる
b7cea000    952K r----  /usr/lib/locale/locale-archive
b7dd8000   2048K r----  /usr/lib/locale/locale-archive
b7fd8000      4K rw---    [ anon ]
b7fec000      4K rw---    [ anon ]
bf88b000     84K rw---    [ stack ]     ★ スタック領域。低位アドレスに向かって伸びる
 total     4748K

 

3. pmap $PID を実行する

% readelf -S /bin/sleep 

There are 32 section headers, starting at offset 0x48ac:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0 0
  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0 1
  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0 4
  [ 3] .note.gnu.build-i NOTE            08048168 000168 000024 00   A  0   0 4
  [ 4] .gnu.hash         GNU_HASH        0804818c 00018c 000040 04   A  5   0 4
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 000310 10   A 28   1 4
  [ 6] .gnu.liblist      GNU_LIBLIST     080484dc 0004dc 000028 14   A 28   0 4
  [ 7] .gnu.conflict     RELA            08048504 000504 0000d8 0c   A  5   0 4
  [ 8] .gnu.version      VERSYM          08048722 000722 000062 02   A  5   0 2
  [ 9] .gnu.version_r    VERNEED         08048784 000784 000080 00   A 28   1 4
  [10] .rel.dyn          REL             08048804 000804 000058 08   A  5   0 4
  [11] .rel.plt          REL             0804885c 00085c 000128 08   A  5  13 4
  [12] .init             PROGBITS        08048984 000984 000017 00  AX  0   0 4
  [13] .plt              PROGBITS        0804899c 00099c 000260 04  AX  0   0 4
  [14] .text             PROGBITS        08048c00 000c00 001fd8 00  AX  0   0 16
  [15] .fini             PROGBITS        0804abd8 002bd8 00001c 00  AX  0   0 4
  [16] .rodata           PROGBITS        0804ac00 002c00 000900 00   A  0   0 32
  [17] .eh_frame_hdr     PROGBITS        0804b500 003500 000164 00   A  0   0 4
  [18] .eh_frame         PROGBITS        0804b664 003664 000574 00   A  0   0 4
  [19] .ctors            PROGBITS        0804cbd8 003bd8 000008 00  WA  0   0 4
  [20] .dtors            PROGBITS        0804cbe0 003be0 000008 00  WA  0   0 4
  [21] .jcr              PROGBITS        0804cbe8 003be8 000004 00  WA  0   0 4
  [22] .data.rel.ro      PROGBITS        0804cc00 003c00 000060 00  WA  0   0 32
  [23] .dynamic          DYNAMIC         0804cc60 003c60 0000c8 08  WA 28   0 4
  [24] .got              PROGBITS        0804cd28 003d28 000030 04  WA  0   0 4
  [25] .got.plt          PROGBITS        0804cd58 003d58 0000a0 04  WA  0   0 4
  [26] .data             PROGBITS        0804cdf8 003df8 000018 00  WA  0   0 4
  [27] .bss              PROGBITS        0804ce20 003e20 000164 00  WA  0   0 32
  [28] .dynstr           STRTAB          0804cf84 003f84 000258 00   A  0   0 1
  [29] .gnu_debuglink    PROGBITS        00000000 0041dc 000010 00      0   0 4
  [30] .gnu.prelink_undo PROGBITS        00000000 0041ec 000594 01      0   0 4
  [31] .shstrtab         STRTAB          00000000 004780 00012c 00      0   0 1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

 
 

75 スタック領域のアドレスを取得する

Linuxはプラットフォームごとにスタック開始アドレスの固定値がある(Linux/i386 => 0xbfffffff)

しかし,カーネル2.6.12からスタック開始アドレスをランダムにずらすスタック保護機能がデフォルトになった。(固定値から最大8MBほどスタックの成長方向にずれる)

これを吸収してスタック開始アドレスを求めるには,glibc内のシンボル __libc_stack_end を用いること。
サンプルコード get_stack_size.c

get_stack_size.c

/* #75 スタック領域のアドレスを取得する */
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>

#pragma weak   __libc_stack_end

extern void  * __libc_stack_end;

void *
get_linux_stack_base(void)
{
  long pagesize   = sysconf(_SC_PAGESIZE);
  long start_addr = 0;

  start_addr = (((uintptr_t)__libc_stack_end + pagesize) & ~(pagesize - 1));

  return (void *)start_addr;
}

int
main()
{
  printf("PID=%d stack start address =  %p\n", getpid(), get_linux_stack_base());

  return 0;
}

 
 

81 SIGSEGV を使ってアドレスの有効性を確認する

sigsetjmp, siglongjmp を使う上での注意点については,非同期シグナルとanti-pattern - memologueの内容を参照.より

/* #81 SIGSEGV を使ってアドレスの有効性を確認する */
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <signal.h>

#define TRUE    1
#define FALSE   0

static struct sigaction  orig_act;
static        sigjmp_buf env;

static void
sigsegv_handler(int sig)
{
  siglongjmp(env, 1);
}

int
validate(void * addr)
{
  int              is_valid = FALSE;
  struct sigaction act;
  
  sigemptyset(&act.sa_mask);
  
  act.sa_flags   = 0;
  act.sa_handler = sigsegv_handler;

  sigaction(SIGSEGV, &act, &orig_act); /* 現在のレジスタを orig_act に格納 */
  
  if(sigsetjmp(env, TRUE) == 0)
  {
    volatile char c;
    c = *((char *)addr); /* read */
    *((char *)addr) = c; /* write */
    is_valid = TRUE;
  }
  else
  {
    is_valid = FALSE;
  }
  
  sigaction(SIGSEGV, &orig_act, NULL);
  
  return is_valid;
}

int
main(int argc, char * argv[])
{
  int a;

  printf("variable a: %s\n", (validate(&a) ? "valid" : "invalid"));
  printf("100       : %s\n", (validate((void *)100) ? "valid" : "invalid"));

  return 0;
}

*1:低階層の知識とはいえ、さすがに10年も経つと本書の内容も現在の環境と乖離しつつある箇所が現れ始めている。可能なら最近の動向に合せて刷新してもらえるとありがたい