13. デバッグ
今回はアセンブリで書いたプログラムのデバッグ方法です.バグのあるアセンブリ プログラムは実行すると簡単に暴走します.MS-DOS ではマシンのリセットしかなく, 保存を忘れた書きかけのプログラムは完全に無くなってしまいます.しかし Linux ではプログラムが暴走しても一般ユーザ権限で実行する限りあまり危険ではありません. プログラムの内容によっては書きこみ権限のあるファイルを壊す可能性はありますが,これはどのような言語でも あり得ます.特にマシンのリセット (リブート) が必要となることはありません.通常は Segmentation fault を表示するか,戻ってこないだけです.戻ってこない場合は 無限ループに陥っていることが考えられるため :
- あわてずに Alt-F2 などで別の端末を開ける.
- ps で戻ってこないプロセスのプロセス番号を確認.
- 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 はプログラムに許されていない領域のメモリをアクセス したために発生するエラーです.
エラーがどこで発生したか調べる方法のうちで,最も基本的 (低レベル) な方法は
- どこまで正常に実行しているか調べてみる.
- 怪しいそうなところでレジスタの値を調べてみる.
- 怪しいそうなところでメモリの内容を調べてみる.
といったところでしょう.実際に以上の方法で殆どのバグの原因は発見できます.
アセンブリプログラムのバグを調査するためのプログラムを作ってみました.どこまで 正常に動作しているかを確認する,バグの潜んでいそうな部分のレジスタやメモリの値 を表示するような機能を持つプログラム(サブルーチン)になっています.確認したい プログラムの動作に影響しないように注意して作成したつもりです.
;=========================================================================
; 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 命令が届かなくなる 場合がありアセンブル時にエラーとなるので注意してください.