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 <[email protected]> ;========================================================================= %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 命令が届かなくなる 場合がありアセンブル時にエラーとなるので注意してください.