저자 Nasser Nouri

소프트웨어의 버그가 쉽게 발견되지 않을때 dbx 커맨드라인 디버거 환경에서 머신레벨의 명령어를 디버깅 하는 것은 유용한 방법입니다. 일반적으로 C, C++, 포트란 같은 고수준의 언어로 쓰여진 대부분의 소프트웨어의 결점은 동일한 고수준의 dbx 환경에서 디버깅이 가능합니다.

그러나 프로그램이 실행되고 있는 시스템의 머신 수준에서의 명령어 지식을 가지고 있다면 적절한 툴, 즉 dbx 같은 툴은 문제점을 찾고 결점을 수정하는 최적의 해결책을 찾는데 걸리는 시간을 단축시켜 줄 수 있습니다.
 
이 글은 AMD64 아키텍쳐에서 dbx 디버거를 효과적으로 사용하는 방법에 대해 설명 합니다. 특정 주소 메모리의 내용을 출력하는 법과 머신수준의 명령어를 출력하는 방법에 대해서 설명합니다. regs 커맨드를 이용해서 머신 레지스터의 내용을 출력하는 방법 혹은 print 커맨드를 이용해서 개별 레지스터들의 내용을 출력하는 방법도 설명합니다. nexti, stepi, stopi, 그리고 tracei 커맨드를 이용해서 AMD64 머신 수준의 디버깅을 해봅니다.

AMD64 아키텍쳐

일단 AMD64 아키텍쳐에 대해 간략히 설명하고 32-비트 x86 아키텍쳐와의 다른점을 살펴 보겠습니다. 필자는 이글에 관련된 사항들에 대해서만 설명할 것입니다. AMD64 아키텍쳐에 대한 더 자세한 정보는 AMD64 메뉴얼 (http://developer.amd.com) 과 AMD64 어플리케이션 바이너리 인터페이스 Binary Interface (ABI) (http://www.x86-64.org) 를 참고하시기 바랍니다.

AMD64 아키텍쳐는 16개의 64-비트 범용 레지스터를 가지고 있습니다 (GPRs): RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, R8, R9, R10, R11, R12, R13, R14, 그리고 R15 입니다. x86 아키텍쳐와 비교해서 AMD64 아키텍쳐는 8개의 새로운 범용 레지스터를 가지고 있습니다.

RAX, RBX, RCX, RDX, RBP, RSI, RDI, 그리고 RSP 레지스터는 32비트와 64비트 바이너리 양쪽에서 모두 사용됩니다. 그러나 32비트 모드에서 32비트 바이너리들은 오직 하위 32비트에만 접근할 수 있습니다. x86 아키텍쳐에서 이러한 레지스터들은 EAX, EBX, ECX, EDX, EBP, ESI, EDI, 그리고 ESP 입니다.

64-비트
AMD 레지스터
32-비트
x86 레지스터
RAX
EAX
RBX
EBX
RCX
ECX
RDX
EDX
RSI
ESI
RDI
EDI
RBP
EBP
RSP
ESP
R8
-
R9
--
R10
--
R11
--
R12
--
R13
--
R14
--
R15
--
RFLAGS
EFLAGS
RIP
EIP


표 1:  범용 레지스터들


AMD64 아키텍쳐는 16개의 128-비트 XMM 레지스터를 제공합니다. 레지스터 XMM0 에서 XMM7 까지는 float 및 double 형 파라미터를 전달하는데 사용 됩니다. long double 타입은 메모리로 전달됩니다. AMD64 아키텍쳐에서 long double 은 16바이트의 길이로써 x86 아키텍쳐에서의 12 바이트와 비교 됩니다. long double 타입은 80-비트 확장(IEEE) 표준으로 구현되어 있습니다.

128-비트
미디어 레지스터
XMM0
XMM1
XMM2
XMM3
XMM4
XMM5
XMM6
XMM7
XMM8
XMM9
XMM10
XMM11
XMM12
XMM13
XMM14
XMM15


표 2:  미디어 레지스터


AMD64 아키텍쳐는 또한 8개의 x87 부동소숫점 레지스터를 제공하고 각각은 80비트 의 길이 입니다.

64-비트 미디어 레지스터 /
80-비트 부동소숫점 레지스터
MMX0 / FPR0
MMX1 / FPR1
MMX2 / FPR2
MMX3 / FPR3
MMX4 / FPR4
MMX5 / FPR5
MMX6 / FPR6
MMX7 / FPR7


표 3: 미디어 / 부동소숫점 레지스터


스택으로 함수 매개변수들이 전달되는 32-비트 아키텍쳐와는 대조적으로 64-비트 아키텍쳐는 정수 파라미터 전달을 위해 6개의 레지스터를 가지고 있습니다. 만약 정수 파라미터의 갯수가 6개를 넘어가면 나머지 파라미터들은 스택으로 전달 됩니다.

bool, char, short, int, long, long long, 그리고 pointer 타입은 정수형 클래스로 분류 됩니다. 정수형 클래스의 파라미터 전달을 위해서 RDI, RSI, RDX, RCX, R8, 그리고 R9 레지스터 중에서 순서대로 가능한 레지스터가 사용됩니다.

레지스터 RBP, RBX, 그리고 R12 에서 R15 는 모두 함수 호출과 관련되어 있는데 이것은 호출 함수가 그 값들을 모두 보존할 필요가 있기 때문입니다.

RIP 레지스터는 명령어 포인터 레지스터 입니다. 64비트 모드에서 RIP 레지스터는 64비트 오프셋을 지원하도록 64비트로 확장되었습니다. 32비트 x86 아키텍쳐에서 명령어 포인터 레지스터는 EIP 레지스터 입니다.

함수의 리턴 값은 AMD64 ABI 에서 지정된 규칙에 기반하여 분류 됩니다. 예를 들어 리턴 값이 메모리상으로 전달되어야 한다면 호출자는 리턴 값을 위한 공간을 제공하고 이 공간의 주소를 RDI 레지스터를 통해서 마치함수의 첫번째 매개변수 인것처럼 전달 합니다. 리턴시에는 RAX 레지스터에 호출자가 RDI 레지스터로 전달했던 주소를 담고 있습니다.

유사하게 만약 리턴 타입이 정수형이면 RAX, RDX 중에서 순서대로 사용가능한 레지스터가 사용 됩니다.

레지스터에 덧붙여서 각 함수들은 런타임 스택의 프레임을 가지고 있습니다. 런타임 스택은 상위 주소로 부터 하위로 확장 됩니다. 표 4는 스택의 구조를 나타 냅니다.

위치
내용
프레임
8n+16 (%rbp)
...

32 (%rbp)
24 (%rbp)
16 (%rbp)
매개변수 #n
...

매개변수 #2
매개변수 #1
매개변수 #0
상위 주소

이전 프레임



8 (%rbp)
리턴 주소
현재 프레임
0 (%rbp)
이전 %rbp
현재 프레임
-8 (%rbp)
-16 (%rbp)
...

0 (%rsp)
로컬 변수 #1
로컬 변수 #2
...

로컬 변수 #n
현재 프레임



하위 주소
-128 (%rsp)
red zone


표 4: 베이스 포인터와 스택 프레임


RSP 레지스터는 스택 포인터 레지스터 이고 RBP 레지스터는 프레임 포인터 레지스터 입니다. 스택 명령어들은 암시적으로 RSP 레지스터를 사용하고 종종 RBP 레지스터를 사용하기도 합니다. RSP 레지스터는 스택에 아이템이 푸시될 때 마다 감소합니다. 그리고 스택에서 빠져나갈때마다 증가합니다. RBP 레지스터는 다른 함수로 데이타 구조가 전달될 때 최하위의 주소를 가르킵니다.

RSP 레지스터에 의해 가르켜 지는 128-바이트 구역 이후는 레드 존이라고 불리며 예약된 곳입니다. 함수는 함수 호출 간에 필요하지 않는 임시 데이타를 저장하는데 이 영역을 사용할 수 있습니다. 특히 곁가지 함수들은 예제 1번(함수 prologue)이나 예제 2번(함수 epilogue) 처럼 스택 포인터를 조정할 필요 없이 이 공간을 전체 스택 프레임을 저장하는데 이용할 수 있습니다.

prologue:
pushq %rbp          / 프레임 포인터 저장
movq %rsp,%rbp      / 새로운 프레임 포인터 설정
subq $48,%rsp       / 스택 공간 할당
movq %rbx,-16(%rbp) / 호출 함수 정보 복구를 위한 레지스터 저장
movq %r12,-24(%rbp)
movq %r13,-32(%rbp)
movq %r14,-40(%rbp)
movq %r15,-48(%rbp)


예제 1: 함수 Prologue


레드 존이 사용되고 있기 때문에 RSP 스택 포인터 레지스터를 조정할 필요가 없습니다. 다시 말해서 subq $48,%rsp 명령은 레드 존이 사용되고 있다면 함수 prologue 에서 필요로 하지 않습니다.

epilogue:
movl -4(%rbp), %eax / set up return value
movq -16(%rbp),%rbx / restore callee-saved registers
movq -24(%rbp),%r12
movq -32(%rbp),%r13
movq -40(%rbp),%r14
movq -48(%rbp),%r15
leave
ret


예제  2: 함수 Epilogue


C++ 언어는 고유의 어플리케이션 바이너리 인터페이스 (ABI) 를 가지고 있습니다. C++ ABI 는 함수 파라미터 값의 전달 및 리턴에 대한 잘 정의된 룰을 가지고 있습니다. C++ ABI 규칙은 AMD64 ABI 규칙 을 같이 제공하고 있습니다; C++ 컴파일러는 AMD64 ABI 규칙과 함께 함수 파라미터 전달에 대한 C++ ABI 규칙 또한 이용해야 합니다.

dbx 커맨드

아래의 커맨드들은 Debugging a Program With dbx 메뉴얼 ( http://docs.sun.com/doc/819-3683) 에 자세히 설명되어 있습니다.

examine [ address ] [ / [ count ] [format ] ]
메모리의 address 부터 시작해서 count 개의 아이템을 format 으로 지정된 형태로 출력해 줍니다

stepi
하나의 머신 명령어를 실행 (함수 내로 이동)

nexti
하나의 머신 명령어를 실행 (함수 다음으로 이동)

listi
소스 라인과 어셈블리 코드를 조합시켜 보여줍니다

tracei
머신 명령어 레벨에서 추적합니다

stopi
브레이크 포인트를 머신 명령어 레벨에서 설정합니다

dis
`+' 값으로 시작되는 부분 부터 10개의 명령어를 디스어셈블 합니다

print expression, ...
표현식(들) expression 의 값을 출력합니다

regs [-f] [-F]
레지스터의 값을 출력합니다
-f: 부동 소숫점 레지스터를 포함 (한자리 정확도)
-F: 부동 소숫점 레지스터를 포함 (두자리 정확도)

문제 상황 Problem Statement

머신 명령어 레벨의 디버깅을 보여드리기 위해 AMD64 플랫폼에서 버그 리포트된 64-비트 dbx 의 버그를 테스트 케이스를 이용해서 살펴 보겠습니다.

AMD64 플랫폼에서 dbx 가 strchr 호출 후에 실제 문자 대신 16진수 값을 출력합니다 :

(dbx) print strchr("hello", 'l') =  0xfffffd7fffdff742 "\xdf\xff^?\xfd\xff\xff^D"
    ===>> 이것은 dbx 의 버그 입니다.

테스트 케이스는 다음과 같습니다:

main() {
  char *b = "hello";
  printf("%s\n", b);
  printf("%s\n", strchr("hello", 'l'));
}

프로그램에는 아무런 문제도 없습니다. 버그는 dbx 에 존재합니다.

dbx 오류

일단 테스트케이스 코드를 차례대로 실행해보면서 dbx 환경에서의 일반적인 프로그램 흐름을 살펴 봅시다.

% dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop in main
(2) stop in main
(dbx) run
Running: a.out
(process id 16245)
stopped in main at line 3 in file "1.c"
3 char *b = "hello";
(dbx) next
stopped in main at line 4 in file "1.c"
4 printf("%s\n", b);
(dbx) next
hello
stopped in main at line 5 in file "1.c"
5 printf("%s\n", strchr("hello", 'l'));
(dbx) next
llo
stopped in main at line 6 in file "1.c"
6 }
(dbx) next
execution completed, exit code is 4

5번째 줄에 print 구문은 strchr 함수를 두개의 파라미터로 호출합니다. strchr 함수는 첫번째 파라미터 hello 를 검색해서 l 문자가 처음 발견된 위치를 리턴 합니다. 그러므로 llo 케릭터 문자는 printf 구문에 의해 정확하게 출력됩니다.

이제 dbx 커맨드에서 print 커맨드를 이용해서 strchr 를 직접 호출해서 오류를 재현해 봅시다. dbxcall 커맨드는 커맨드라인에서 strchr 함수를 호출하는데에도 사용할 수 있습니다.

% dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop in main
(2) stop in main
(dbx) run
Running: a.out (process id 14772)
stopped in main at line 3 in file "1.c"<
3 char *b = "hello";
(dbx) next stopped in main at line 4 in file "1.c"
4 printf("%s\n", b);
(dbx) next hello
stopped in main at line 5 in file "1.c"
5 printf("%s\n", strchr("hello", 'l'));
(dbx) print strchr("hello", 'l')
strchr("hello", 'l') = 0xfffffd7fffdff742 "\xdf\xff^?\xfd\xff\xff^D"

dbxprint 커맨드에 의해 strchr 이 호출 되었을때 부정확한 결과를 출력합니다. dbx 는 16진수 문자들 대신 llo 을 출력해야 합니다. 왜냐하면 strchr 함수의 호출은 문자 hello 에서 처음 l 문자가 발견된 위치의 포인터를 리턴해야 하기 때문입니다.


디버깅 세션

a.out 실행파일로 디버거를 실행 한 후에 printf 구문 바로 뒤에서 정지해 봅시다. strchr 함수는 libc 라이브러리에 정의되어 있고 거의 대부분 -g 옵션으로 컴파일되어 있지 않습니다. 그러므로 어떠한 디버깅 정보도 존재하지 않으므로 우리는 오직 어셈블리 코드에 의존해야 합니다.

stopi 커맨드는 strchr 함수에서의 첫번째 머신 명령어에 브레이크포인트를 거는데에 사용됩니다.

% dbx a.out
Reading a.out
Reading ld.so.1
Reading libc.so.1
(dbx) stop in main
(2) stop in main
(dbx) run
Running: a.out (process id 15045)
stopped in main at line 3 in file "1.c"
3 char *b = "hello";
(dbx) next
stopped in main at line 4 in file "1.c"
4 printf("%s\n", b);
(dbx) next
hello
stopped in main at line 5 in file "1.c"
5 printf("%s\n", strchr("hello", 'l'));
(dbx) stopi at strchr
(3) stopi at &strchr
(dbx) print strchr("hello", 'l')
stopped in strchr at 0xfffffd7fff307910
0xfffffd7fff307910: strchr : movb& (%rdi),%dl
<p ="">dbxprint 커맨드를 사용해서 strchr 함수가 호출된 다음에 strchr 의 제일 첫번째 명령어를 출력해 줍니다.

dis 커맨드는 strchr 함수 초반의 머신 명령어를 출력하는데 사용됩니다.

(dbx) dis strchr 
0xfffffd7fff307910: strchr       : movb (%rdi),%dl 
0xfffffd7fff307912: strchr+0x0002: cmpb %dh,%dl
0xfffffd7fff307915: strchr+0x0005: je strchr+0x3f [0xfffffd7fff30794f, .+0x3a ]
0xfffffd7fff307917: strchr+0x0007: testb %dl,%dl
0xfffffd7fff307919: strchr+0x0009: je strchr+0x33 [0xfffffd7fff307943, .+0x2a ]
0xfffffd7fff30791b: strchr+0x000b: movb 0x0000000000000001(%rdi),%dl
0xfffffd7fff30791e: strchr+0x000e: mpb %dh,%dl
0xfffffd7fff307921: strchr+0x0011: je strchr+0x3c [0xfffffd7fff30794c, .+0x2b ]
0xfffffd7fff307923: strchr+0x0013: testb %dl,%dl
0xfffffd7fff307925: strchr+0x0015: je strchr+0x33 [0xfffffd7fff307943, .+0x1e ]

strchr 함수의 첫번째 명령은 movb (%rdi),%dl 이고 이것은 %rdi 레지스터가 가르키는 메모리 위치의 내용을 그 자신의 하위 8비트로 이동시킵니다. 첫번째 명령이 pushq %rbp 명령이 아닌 것은 strchr 함수가 prologue 를 가지고 있지 않다는 의미 입니다. 함수가 prologue 가 없다는 것이 문제가 되지는 않습니다.

디버거는 첫번째 명령어에서 정지하게 되고 이곳이 바로 입력 파라미터가 정확하게 strchr 함수에 전달 됐는지 확인 할 수 있는 곳입니다. strchr 함수는 두개의 파라미터를 가지고 있습니다. 첫번째 파라미터는 hello 문자열 스트링의 메모리상에 위치를 가르키는 포인터이고 두번째 파라미터는 문자 l 입니다.

AMD64 ABI 에 기반하여 첫번째와 두번째 파라미터는 %rdi%rsi 레지스터에 순서대로 저장됩니다.

%rdi%rsi 레지스터의 내용을 출력하는 방법은 두가지가 있습니다.

  • print 커맨드를 이용하면 개별 레지스터의 내용을 출력할 수 있습니다. -flx 옵션은 dbx%rdi%rsi 레지스터를 long 16진수 포맷으로 출력하도록 강제 합니다.
    (dbx) print -flx $rdi 
    $rdi = 0xfffffd7fffdff740
    (dbx) print -flx $rsi
    $rsi = 0x6c
  • regs 커맨드를 이용해서 AMD64 레지스터의 모든 내용을 출력할 수 있습니다.
    (dbx) regs
    current frame: [1]
    r15         0x0000000000000000
    r14         0x0000000000000000
    r13         0x0000000000000000
    r12         0x0000000000000000
    r11         0xfffffd7fff307910
    r10         0x0000000000000000
    r9          0x0000000000010000
    r8          0xfefeff6e6b6b6467
    rdi         0xfffffd7fffdff740
    rsi         0x000000000000006c
    rbp         0xfffffd7fffdff810
    rbx         0xfffffd7fff3fb190
    rdx         0x0000000000000000
    rcx         0x000000003f570d87
    rax         0x0000000000000000
    trapno      0x0000000000000003
    err         0x0000000000000000
    rip         0xfffffd7fff307910:strchr movb (%rdi),%dl
    cs          0x000000000000004b
    eflags      0x0000000000000282
    rsp         0xfffffd7fffdff738
    ss          0x0000000000000043
    fs          0x00000000000001bb
    gs          0x0000000000000000
    es          0x0000000000000000
    ds          0x0000000000000000
    fsbase      0xfffffd7fff3a2000
    gsbase&     0xffffffff80000000
      

%rdi 레지스터는 메모리 0xfffffd7fffdff740 에 대한 포인터를 포함하고 있고 스택에 할당되어 있습니다. 정상적인 프로그램의 흐름에서 %rdi 레지스터는 데이타 세그먼트 내의 메모리를 가르키는 포인터를 포함하고 있습니다. 그러나 dbx 가 함수 (strchr) 를 호출하도록 요청 받았을때 dbx 는 데이타 세그먼트의 메모리 위치를 스택에 복사하고 스택 주소를 %rdi 레지스터에 전달합니다.

메모리 위치 0xfffffd7fffdff740 의 내용은 examine 커맨드를 이용해서 검사해 볼 수 있습니다. 메모리는 반드시 hello 문자열 스트링을 가지고 있어야 합니다.

(dbx) examine 0xfffffd7fffdff740 / 2
0xfffffd7fffdff740: 0x6c6c6568 0x0000006f

ASCII 테이블을 검색해 보면 실제로 메모리 위치 0xfffffd7fffdff740hello 문자열 스트링을 가지고 있음을 확인할 수 있습니다 16 진수 숫자 68 은 문자 h, 65e, 6cl, 그리고 6fo 를 나타냅니다.

examine 커맨드를 직접 이용해서 메모리 위치 0xfffffd7fffdff740 의 내용을 ASCII 테이블을 참조하지 않고도 문자열 스트링으로 확인해 볼 수 있습니다.

(dbx) examine 0xfffffd7fffdff740 / 6c
0xfffffd7fffdff740: 'h' 'e' 'l' 'l' 'o' '\0'

%rsi 레지스터는 16진수 6c 를 가지고 있고 이것은 l 문자를 가르킵니다.

다른 두개의 중요한 레지스터는 %rsp (스택 포인터) 와 %rbp (프레임 포인터) 입니다. %rsp 레지스터는 스택의 최상단을 가르키고 그 값은 0xfffffd7fffdff738 입니다. 여러분이 보듯이 이 값은 hello 케릭터 문자열을 포함하고 있는 스택상의 메모리 %rdi 레지스터의 내용과 매우 가깝습니다.

%rbp 레지스터는 프레임 포인터이고 0xfffffd7fffdff81 값을 가지고 있습니다. %rbp 레지스터는 strchr 함수에서 사용되지 않습니다.

런타임 스택은 examine 커맨드를 이용해서 출력할 수 있습니다.

(dbx) examine 0xfffffd7fffdff738 / 32 lx
0xfffffd7fffdff738: 0xfffffd7fff220004 0x0000006f6c6c6568
0xfffffd7fffdff748: 0x0000000000000000 0x0000000000000000
0xfffffd7fffdff758: 0x0000000000000000 0xfffffd7fffdff7b0
0xfffffd7fffdff768: 0xfffffd7fff3c7e50 0x0000000000010000
0xfffffd7fffdff778: 0x0000000000000000 0x0000000000410c50
0xfffffd7fffdff788: 0x0000000000000000 0xfffffd7fffdff848
0xfffffd7fffdff798: 0x0000000000410c50 0x0000000000410c58
0xfffffd7fffdff7a8: 0xfffffd7fff3fb190 0x0000000000000000
0xfffffd7fffdff7b8: 0x0000000000000000 0xfffffd7fffdff810
0xfffffd7fffdff7c8: 0x000000000040099d 0x0000000000000000
0xfffffd7fffdff7d8: 0x0000000000000000 0x0000000000000000
0xfffffd7fffdff7e8: 0x0000000000000000 0x0000000000000000
0xfffffd7fffdff7f8: 0xfffffd7fff3fb190 0x0000000000410c50
0xfffffd7fffdff808: 0xfffffd7fffdff838 0xfffffd7fffdff820
0xfffffd7fffdff818: 0x000000000040080c 0x0000000000000000
0xfffffd7fffdff828: 0x0000000000000000 0x0000000000000001

사실 이전 섹션에서 우리가 배웠던 베이스 포인터와 스택 프레임을 이용해서 런타임 스택을 풀어 볼 수 있습니다. 예를 들어 16진 수 0x40080ccallq 함수 이후에 수행될 명령어의 주소 입니다. main 함수는 _start 에 의해 callq 함수를 이용해서 호출 됩니다.

16진수 0x40080cmain 함수를 호출 하기 전에 스택에 푸시된 리턴 주소입니다. 주소 0x40080c 의 명령 push %raxmain 함수 완료 후에 실행될 것입니다. 다시 말해서 주소 0x40080c 는 프로그램 카운터에 로드될 것이고 main 함수가 리턴된 다음에는 %rip 레지스터에 저장될 것입니다.

objdump 유틸리티 프로그램을 이용해서 실행파일의 텍스트 섹션을 덤프할 수 있습니다.

 objdump -S a.out

00000000004007a0 <_start>:

  4007a0:    6a 00                   pushq  $0x0
  4007a2:    6a 00                   pushq  $0x0
  4007a4:    48 8b ec                mov    %rsp,%rbp
  4007a7:    48 8b fa                mov    %rdx,%rdi
  4007aa:    48 c7 c0 80 0a 41 00    mov    $0x410a80,%rax
  ...

  400806:    59                      pop    %rcx
  400807:    e8 54 01 00 00          callq  400960 <main>
  40080c:    50                      push   %rax
  40080d:    50                      push   %rax 
  ...


main 함수의 첫번째 명령어는 push %rbp 입니다 그러므로 이전 프레임 포인터 (0xfffffd7fffdff820) 는 리턴 주소 바로 이후에 스택에 푸시 됩니다.

유사하게 리턴 주소 (0x40099d) 는 strchr 함수가 커맨드라인에서 호출 됐을때 스택에 저장됩니다.

0000000000400960 <main>:
  400960:    55                      push
  400961:    48 8b                   mov    %rsp,%rbp
  400964:    48 83 ec 40             sub    $0x40,%rsp
  ...

  40099d:    b8 6c 00 00 00          mov    $0x6c,%eax
  4009a2:    0f be f0                movsbl %al,%esi
  4009a5:    48 c7 c7 68 0c 41 00    mov    $0x410c68,%rdi
  4009ac:    b8 00 00 00 00          mov    $0x0,%eax


However, the strchr 함수는 함수 prologue 를 가지고 있지 않으므로 %rbp 레지스터의 내용은 여전히 main 함수에서 strchr 이 호출돼었을때와 동일합니다. %rbp 레지스터의 내용은 16진수 0xfffffd7fffdff810 이고 따라서 0xfffffd7fffdff810 주소의 내용은 이전의 프로엠 포인터 0xfffffd7fffdff820 를 가르키게 됩니다.

(dbx) examine 0xfffffd7fffdff810
0xfffffd7fffdff810: 0xfffffd7fffdff820

좀 더 나아가서 nexti 커맨드를 이용해서 한단계씩 머신 명령어를 수행하여 %rax 레지스터내의 리턴값을 리턴하는 함수까지 진행해 봅시다. dis 커맨드를 이용해서 strchr 함수의 마지막 부분의 머신 명령어 부분들을 출력할 수 있습니다.

(dbx) dis
0xfffffd7fff307941: strchr+0x0031: jne strchr [0xfffffd7fff307910, .-0x31 ]
0xfffffd7fff307943: strchr+0x0033: xorl %eax,%eax
0xfffffd7fff307945: strchr+0x0035: ret
0xfffffd7fff307946: strchr+0x0036: incq %rdi
0xfffffd7fff307949: strchr+0x0039: incq %rdi
0xfffffd7fff30794c: strchr+0x003c: incq %rdi
0xfffffd7fff30794f: strchr+0x003f: movq %rdi,%rax
0xfffffd7fff307952: strchr+0x0042: ret
0xfffffd7fff307953: strchr+0x0043: addb %al,(%rax)
(dbx) nexti
stopped in strchr at 0xfffffd7fff307949
0xfffffd7fff307949:
strchr+0x0039:
incq %rdi
(dbx) nexti
stopped in strchr at 0xfffffd7fff30794c
0xfffffd7fff30794c: strchr+0x003c: incq %rdi
(dbx) nexti
stopped in strchr at 0xfffffd7fff30794f
0xfffffd7fff30794f: strchr+0x003f: movq %rdi,%rax
(dbx) nexti
stopped in strchr at 0xfffffd7fff307952
0xfffffd7fff307952: strchr+0x0042: ret

strchr 함수에 설명에 의하면 마지막에 문자열 hello 에 첫번째로 나타난 l 에 위치를 가르키는 포인터를 리턴하도록 되어 있습니다. %rax 레지스터의 내용을 검사해 봄으로써 strchr 함수의 정확성을 확인해 볼 수 있습니다.

(dbx) examine $rax / 4c
0xfffffd7fffdff742:     'l' 'l' 'o' '\0'
실제로 %rax 레지스터의 값은 메모리 0xfffffd7fffdff742 를 가르키는 포인터이고 스택에 할당되어 있으며 llo 문자열 스트링을 포함하고 있습니다.

우리는 strchr 함수가 정상적으로 동작하고 있음을 확인했고 %rax 레지스터가 llo 문자열을 가르키고 있는 포인터를 리턴한다는 것도 확인했습니다. 그러므로 문제는 dbxstrchr 함수를 호출하고 난뒤에 내부적으로 하고 있는 작업에 있음이 확인되었습니다.

좀 더 진도를 빠르게 나가자면 유저 함수를 호출한 후에 dbx 는 항상 fflush 를 호출하여 출력 스트림을 flush 합니다. fflush 스트림은 하나의 파라미터를 취하는데 그것은 FILE 데이타 구조의 포인터 입니다.

fflush - flush a stream
#include <stdio.h>
int fflush(FILE *stream);

dis 커맨드를 이용해서 fflush 함수의 머신 명령어를 출력할 수 있습니다.

(dbx) dis fflush
0xfffffd7fff33dca0: fflush: pushq %rbp 0xfffffd7fff33dca1: fflush+0x0001: movq %rsp,%rbp
0xfffffd7fff33dca4: fflush+0x0004: movq %rbx,0xfffffffffffffff0(%rbp)
0xfffffd7fff33dca8: fflush+0x0008: movq %r12,0xfffffffffffffff8(%rbp)
0xfffffd7fff33dcac: fflush+0x000c: subq $0x0000000000000010,%rsp
0xfffffd7fff33dcb0: fflush+0x0010: testq %rdi,%rdi
0xfffffd7fff33dcb3: fflush+0x0013: movq %rdi,%rbx

fflush 함수의 prologue 를 살펴 봅시다:

pushq %rbp

이전의 프레임 포인터를 스택에 저장합니다.

movq %rsp, %rbp

%rsp 레지스터의 값을 저장하거나 이전 스택 포인터를 %rbp 레지스터에 저장합니다.이 값은 fflush 함수의 새로운 프레임 포인터 입니다.

movq %rbx,0xfffffffffffffff0(%rbp)
movq %r12,0xfffffffffffffff8(%rbp)

%rbx 레지스터와 %r12 레지스터는 호출시에 저장되는 레지스터 입니다. fflush 함수는 반드시 이 레지스터의 내용들을 호출 함수의 스택에 저장해 두어야 호출된 함수가 종료되기 바로전의 함수 epilogue 에서 상태가 되돌려 질 수 있습니다.

sub $0x0000000000000010,%rsp

fflush 함수의 스택 포인터를 조정해 봅시다.

stopi 커맨드를 이용해서 fflush 함수의 첫번째 명령에서 정지시킵니다.

(dbx) stopi at fflush
(dbx) cont
dbx: Call to 'strchr' completed. Going back to previous command
interpreter
stopped in fflush at 0xfffffd7fff33dca0

0xfffffd7fff33dca0: fflush: pushq    %rbp

dbxdbx 커맨드 라인에서 cont 가 입력된 다음 fflush 함수의 첫번째 명령어에서 정지 합니다.

%rdi, %rbp, 그리고 %rsp 레지스터를 출력해 봅시다. %rdi 레지스터는 FILE 데이타 구조의 포인터 입니다.
(dbx) print -flx $rdi
$rdi = 0xfffffd7fff37f0a0
(dbx) print -flx $rbp
$rbp = 0xfffffd7fffdff810
(dbx) print -flx $rsp
$rsp = 0xfffffd7fffdff748

함수 prologue 까지 진행한 다음 %rsp%rbp 레지스터를 다시한번 출력해 봅시다.

(dbx) stepi
stopped in fflush at 0xfffffd7fff33dca1
0xfffffd7fff33dca1: fflush+0x0001: movq %rsp,%rbp
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dca4
0xfffffd7fff33dca4: fflush+0x0004: movq %rbx,0xfffffffffffffff0(%rbp)
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dca8
0xfffffd7fff33dca8: fflush+0x0008: movq %r12,0xfffffffffffffff8(%rbp)
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dcac
0xfffffd7fff33dcac: fflush+0x000c: subq $0x0000000000000010,%rsp
(dbx) stepi
stopped in fflush at 0xfffffd7fff33dcb0
0xfffffd7fff33dcb0: fflush+0x0010: testq %rdi,%rdi
(dbx) print -flx $rbp
$rbp = 0xfffffd7fffdff740
(dbx) print -flx $rsp
$rsp = 0xfffffd7fffdff730

여러분이 이전 섹션의 내용을 기억한다면 런타임 스택은 상위 주소에서 부터 확장됨을 기억할 것입니다. %rsp 레지스터를 검사해서 그 값 (0xfffffd7fffdff730) 을 strchr 함수 내의 %rsp 레지스터의 마지막 값과 비교해 보면 fflush 함수의 스택에 할당된 공간과 strchr 함수의 영역이 서로 겹치고 있음을 확인 할 수 있습니다.

0xfffffd7fffdff738 값은 %rbp 레지스터의 값 (0xfffffd7fffdff740) 과 fflush 함수의 %rsp 레지스터 (0xfffffd7fffdff730) 범위 안에 존재 합니다. fflush function. 그러므로 fflush 함수는 strchr 함수의 런타임 스택의 내용을 덮어 쓰고 이것이 바로 print strchr("hello", 'l')llo 문자열 대신 쓰레기 값을 출력하는 이유 입니다.

dbx 디버거의 수정판은 fflush 함수를 호출하기 바로 전에 런타임 스택의 내용을 보존하고 print 커맨드로 리턴하기 바로 전에 복구 합니다.

결론

일반적으로 저수준의 디버깅은 사용자에게 프로그램이 실행되는 시스템에 대한 일정 수준의 지식이 필요합니다. 그러나 한번 그 지식을 익히고 나면 아무리 어려운 버그라도 저수준의 디버깅 기술을 이용해서 그리고 dbx 같은 적절한 툴을 이용해서 잡아 낼 수 있습니다.

x86 어셈블리 언어에 대한 지식은 Assembly Language Techniques for the Solaris OS, x86 Platform Edition( http://developers.sun.com/solaris/articles/x86_assembly_lang.html) 에서 얻으 실 수 있습니다.



이 글의 원본은 http://developers.sun.com/solaris/articles/x64_dbx.html 에서 보실 수 있습니다.

"개발자코너" 카테고리의 다른 글

2007/12/14 10:05 2007/12/14 10:05

TRACKBACK :: http://blog.sdnkorea.com/blog/trackback/482

댓글을 달아 주세요

[로그인][오픈아이디란?]

◀ Prev 1  ... 167 168 169 170 171 172 173 174 175  ... 624  Next ▶