13. デバッグ

今回はアセンブリで書いたプログラムのデバッグ方法です.バグのあるアセンブリ プログラムは実行すると簡単に暴走します.MS-DOS ではマシンのリセットしかなく, 保存を忘れた書きかけのプログラムは完全に無くなってしまいます.しかし Linux ではプログラムが暴走しても一般ユーザ権限で実行する限りあまり危険ではありません. プログラムの内容によっては書きこみ権限のあるファイルを壊す可能性はありますが,これはどのような言語でも あり得ます.特にマシンのリセット (リブート) が必要となることはありません.通常は Segmentation fault を表示するか,戻ってこないだけです.戻ってこない場合は 無限ループに陥っていることが考えられるため :

  1. あわてずに Alt-F2 などで別の端末を開ける.
  2. ps で戻ってこないプロセスのプロセス番号を確認.
  3. kill プロセス番号で終了させる.

以上で無事にシェルのプロンプトに戻ることができます.

さて,実際に Segmentation fault となって正常に終了しないプログラムを作成 して実験してみましょう.

;=========================================================================
; Segmentation fault になる
;   file : debug01.asm
;   2000/12/10 Jun Mizutani
;=========================================================================
section .text
global _start

msg             db      'I could die.', 10
msglen          equ     $ - msg

_start:
                mov dword[count], msglen
                mov     ecx, msg
        .loop:  mov     edx, 1              ; 1文字づつ出力
                mov     eax, 4
                mov     ebx, 1              ; 標準出力
                int     0x80
                inc     ecx                 ; 文字位置更新
                dec     dword[count]
                jne .loop
                mov     eax, [esi]          ; !
                mov     eax, 1
                mov     ebx, 0
                int     0x80                ; 終了

section .data

count       dd  0

;=========================================================================

是非,実際にこのプログラムを実行してエラーを体験してみてください.


実行結果1

実際に実行してエラーが起こるか確認してみることにします

jm:~/ex_asm$ asm debug01
jm:~/ex_asm$ ./debug01
I could die.
Segmentation fault
jm:~/ex_asm$

無事に Segmentation fault でプログラムが終了しましたか ? この Segmentation fault はプログラムに許されていない領域のメモリをアクセス したために発生するエラーです.

エラーがどこで発生したか調べる方法のうちで,最も基本的 (低レベル) な方法は

  1. どこまで正常に実行しているか調べてみる.
  2. 怪しいそうなところでレジスタの値を調べてみる.
  3. 怪しいそうなところでメモリの内容を調べてみる.

といったところでしょう.実際に以上の方法で殆どのバグの原因は発見できます.

アセンブリプログラムのバグを調査するためのプログラムを作ってみました.どこまで 正常に動作しているかを確認する,バグの潜んでいそうな部分のレジスタやメモリの値 を表示するような機能を持つプログラム(サブルーチン)になっています.確認したい プログラムの動作に影響しないように注意して作成したつもりです.

;=========================================================================
; DEBUG
; file : debug.inc
; 2000/12/09
;         Copyright (C) 2000 Jun Mizutani <mizutani.jun@nifty.ne.jp>
;=========================================================================
%ifndef __DEBUG_INC
%define __DEBUG_INC

%include "stdio.inc"

;=========================================================================
;-------------------------------------------------------------------------
; 文字列表示
;   例.  CHECK 'Waw!'  ; 4文字まで
;-------------------------------------------------------------------------
%macro CHECK 1
                push    eax
                mov     eax, %1
                call    check_point
                pop     eax
%endmacro

;-------------------------------------------------------------------------
; Register 表示
;   例.  REGS
;-------------------------------------------------------------------------
%macro REGS  0
                call    print_regs
%endmacro

;-------------------------------------------------------------------------
; 指定したメモリの内容を16進数と10進数で表示
;   例.  MEM1    [work]  ; work の1バイトの内容を表示
;        MEM2    [work]  ; work の2バイトの内容を表示
;        MEM4    [work]  ; work の4バイトの内容を表示
;-------------------------------------------------------------------------
%macro MEM1  1
                pushfd
                push    eax
                push    ebx
                xor     eax, eax
                mov     al, %1
                lea     ebx, %1
                call    print_mem1
                pop     ebx
                pop     eax
                popfd
%endmacro

%macro MEM2  1
                pushfd
                push    eax
                push    ebx
                xor     eax, eax
                mov     ax, %1
                lea     ebx, %1
                call    print_mem2
                pop     ebx
                pop     eax
                popfd
%endmacro

%macro MEM4  1
                pushfd
                push    eax
                push    ebx
                mov     eax, %1
                lea     ebx, %1
                call    print_mem4
                pop     ebx
                pop     eax
                popfd
%endmacro

;=========================================================================
section .text

;-------------------------------------------------------------------------
; 文字列表示
; ex.  check 'Waw!'  4文字まで
;-------------------------------------------------------------------------
check_point:
                pushfd
                push    edx
                push    eax
                mov     eax, CheckMsg
                call    OutAsciiZ
                pop     eax
                call    OutChar4
                mov     al, ')'
                call    OutChar
                call    NewLine
                pop     edx
                popfd
                ret

;-------------------------------------------------------------------------
; Register 表示
;-------------------------------------------------------------------------
print_regs:
                pushfd
                push    edx
                push    eax
                call    NewLine
                push    edx                     ; EDX を保存
                push    eax
                mov     eax, 'EAX:'             ; EAX レジスタ表示
                call    OutChar4
                pop     eax
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'EBX:'             ; EBX レジスタ表示
                call    OutChar4
                mov     eax, ebx
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'ECX:'             ; ECX レジスタ表示
                call    OutChar4
                mov     eax, ecx
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'EDX:'             ; EDX レジスタ表示
                call    OutChar4
                pop     eax                     ; 保存していたEDX
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'EDI:'             ; EDI レジスタ表示
                call    OutChar4
                mov     eax, edi
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'ESI:'             ; ESI レジスタ表示
                call    OutChar4
                mov     eax, esi
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'EBP:'             ; EBP レジスタ表示
                call    OutChar4
                mov     eax, ebp
                call    PrintHex8
                call    .PrintDecimal

                mov     eax, 'ESP:'             ; ESP レジスタ表示
                call    OutChar4
                mov     eax, esp
                call    PrintHex8
                call    .PrintDecimal
                pop     eax
                pop     edx
                popfd
                ret
.PrintDecimal
                push    eax
                mov     eax, ' '
                call    OutChar
                pop     eax
                call    PrintLeft
                call    NewLine
                ret

;-------------------------------------------------------------------------
; 指定したメモリの内容を16進数と10進数で表示
;-------------------------------------------------------------------------
print_mem1:
                push    edx
                push    eax
                call    NewLine
                mov     eax, 'MEM:'
                call    OutChar4
                mov     eax, ebx
                call    PrintHex8
                mov     eax, ' '
                call    OutChar
                pop     eax
                call    PrintHex2
                call    print_regs.PrintDecimal
                pop     edx
                ret

print_mem2:
                push    edx
                push    eax
                call    NewLine
                mov     eax, 'MEM:'
                call    OutChar4
                mov     eax, ebx
                call    PrintHex8
                mov     eax, ' '
                call    OutChar
                pop     eax
                call    PrintHex4
                call    print_regs.PrintDecimal
                pop     edx
                ret

print_mem4:
                push    edx
                push    eax
                call    NewLine
                mov     eax, 'MEM:'
                call    OutChar4
                mov     eax, ebx
                call    PrintHex8
                mov     eax, ' '
                call    OutChar
                pop     eax
                call    PrintHex8
                call    print_regs.PrintDecimal
                pop     edx
                ret

;-------------------------------------------------------------------------
; EDI からの10バイト(80bit)のメモリ内容を16進数で表示
; メモリの後部が上位桁
;-------------------------------------------------------------------------
print_mem80:
                pushfd
                push    ecx
                mov     ecx, 10
          .loop
                mov     al, [edi+ecx-1]
                push    edx
                call    PrintHex2
                pop     edx
                loop    .loop
                pop     ecx
                popfd
                ret

;-------------------------------------------------------------------------
; EDI からの10バイト(80bit)のメモリ内容を16進数で表示
; メモリの前部が上位桁
;-------------------------------------------------------------------------
print_10:
                pushfd
                push    edi
                push    ecx
                mov     ecx, 10
          .loop
                mov     al, [edi]
                call    PrintHex2
                inc     edi
                loop    .loop
                pop     ecx
                pop     edi
                popfd
                ret

;-------------------------------------------------------------------------
; FPU のスタックトップを16進数で表示
;     1 - 99京 までの整数を表示可能,超えると FFFFC000000000000000 となる
;-------------------------------------------------------------------------
print_top:
                pushfd
                push    edx
                push    eax
                push    edi
                fld     st0   ; DUP
                fstp    tword [BCD0] ; stack topを指定メモリ10byteに格納
                mov     edi, BCD0
                call    print_mem80
                mov     eax, ':'
                call    OutChar
                fld     st0   ; DUP
                fbstp   tword [BCD0] ;stack topをBCD18桁でメモリ10byteに格納
                mov     edi, BCD0
                call    print_mem80
                call    NewLine
                pop     edi
                pop     eax
                pop     edx
                popfd
                ret

;-------------------------------------------------------------------------
; FPU のステイタスワードを4桁の16進数で表示
;-------------------------------------------------------------------------
print_fpu_status:
                pushfd
                push    eax
                fnstsw  ax
                push    edx
                shr     eax, 11
                and     eax, 7
                call    PrintHex4
                call    NewLine
                pop     edx
                pop     eax
                popfd
                ret

;-------------------------------------------------------------------------
CheckMsg           db 10, 'CHECK(', 0

;=========================================================================
section .bss

BCD0            rest    1       ; 仮数値 BCD 表現(10バイト)

%endif

調べたいプログラムに %include "debug" を追加します.適当なところに CHECK '文字' を挿入するとプログラムの実行がその部分を通ったところで CHECK(文字) が標準出力に出力されます.REGS を挿入するとその時点の レジスタの値が出力されます.また MEM1 [count] でアドレスが count の1バイトの メモリの内容を表示します.MEM2 では2バイト, MEM4 では4バイトで表示します.

FPU を使うプログラム用に print_top と print_fpu_status も用意しました.
単に call print_top のように対象とするプログラムに挿入して使います.

最初にエラーが発生したプログラムに追加して試してみましょう.

;=========================================================================
; Segmentation fault になる
;   file : debug02.asm
;   2000/12/10 Jun Mizutani
;=========================================================================

%include "debug.inc"

section .text
global _start

msg             db      'I could die.', 10
msglen          equ     $ - msg

_start:
REGS
                mov dword[count], msglen
                mov     ecx, msg
CHECK '1'
        .loop:  mov     edx, 1              ; 1文字づつ出力
                mov     eax, 4
                mov     ebx, 1              ; 標準出力
                int     0x80
MEM4  [count]
                inc     ecx                 ; 文字位置更新
                dec     dword[count]
                jne .loop
CHECK '2'
REGS
                mov     eax, [esi]          ; !
CHECK '3'
                mov     eax, 1
                mov     ebx, 0
                int     0x80                ; 終了

section .data

count       dd  0

;=========================================================================

正常に動作すればレジスタの内容とメモリ [count] の内容を表示しながら, CHECK(1), CHECK(2), CHECK(3) と表示して終了するはずです.


実行結果2

実行した結果を示します.

jm:~/ex_asm$ asm debug02
jm:~/ex_asm$ ./debug02

EAX:00000000 0
EBX:00000000 0
ECX:00000000 0
EDX:00000000 0
EDI:00000000 0
ESI:00000000 0
EBP:00000000 0
ESP:BFFFFA30 -1073743312

CHECK(1...)
I
MEM:08049500 0000000D 13

MEM:08049500 0000000C 12
c
MEM:08049500 0000000B 11
o
MEM:08049500 0000000A 10
u
MEM:08049500 00000009 9
l
MEM:08049500 00000008 8
d
MEM:08049500 00000007 7

MEM:08049500 00000006 6
d
MEM:08049500 00000005 5
i
MEM:08049500 00000004 4
e
MEM:08049500 00000003 3
.
MEM:08049500 00000002 2


MEM:08049500 00000001 1

CHECK(2...)

EAX:00000001 1
EBX:00000001 1
ECX:08048482 134513794
EDX:00000001 1
EDI:00000000 0
ESI:00000000 0
EBP:00000000 0
ESP:BFFFFA30 -1073743312
Segmentation fault

CHECK(2...)とレジスタを表示した後に Segmentation fault が発生しています. CHECK(3...)は表示されていないため,mov eax, [esi]でエラーと なっていることが分かります.レジスタの内容を見ると esi が 0 となっているため, メモリの 0 番地を参照しようとして Segmentation fault となっていることが 分かります.

この例では不要な mov eax, [esi] を削除すれば正常に動作します. ちょっとわざとらしい例でしたがレジスタの初期化忘れなどで起こりやすいバグです.

フラッグレジスタまで保存しているので debug.inc を加えてもプログラムの動作に 影響を与えないはずですが,プログラムサイズが大きくなって jump 命令が届かなくなる 場合がありアセンブル時にエラーとなるので注意してください.