프로그램의 오류와 정정

main.c 소스 설명 : 스택 메모리의 저장 모습을 표현하고, test 함수 호출 시 리턴 어드레스의 주소를 main함수 쪽 이 아닌 test2 로 바꾼 다음 “test2” 출력 하고, 프로그램이 main함수 쪽 으로 돌아가지 못해 정상적인 종료를 하지 못하는 프로그램이다.

 

디버그 모드 실행

Linux version 2.4.18-4 (root@localhost.localdomain) (gcc version 2.95.4 20010319 (prerelease)) #1 SMP Thu Aug 22 18:36:08 KST 2002

gcc –g 옵션 사용하여 컴파일

소스코드

<파일 main.c>

#include "HexaView.h"
void test(intint);
int main()
{
  int A 0x12345678;
  int B 0xABCDEFBA;
  printf("[%08X] : main() address\n", main);

  test(A, B);
  return 0;
}

void test2()
{
  printf("test2\n");
}
void test(int a, int b)
{
  int C  0x11223344;
  int *p  &C;
  p = p+2;
  *p = (int)test2;
  PrintHexaNAscii(&C, 100);
  
}

 

확인을 위해 스택영역을 Hexa code로 나타내었다.

 

 gdb01.png

<그림 1-1>

 

일단 코드의 라인1 에 브레이크 포인트를 잡았다. 디버그를 실행시킨후

레지스터의 값들을 확인해 보자

 

(gdb) info frame

 gdb02_d.png

 info reg esp

<그림 1-2>

 

아래의 선이 ebp이고 위 선이 esp이다. 이 사이가 main함수의 스택영역을 가리킨다.

 

ebp의 역할

esp pop이나 push같이 스택에 값을 널거나 빼낼 때 유동적으로 값이 변하면서 이동을 한다. 스택포인터는 항상 스택의 가장 윗부분을 가르키고 있어야 하기 때문이다. 따라서 지역변수에 접근하기 위해서는 상대적으로 거리가 항상 일정한 어떠한 기준점이 필요하게 된 것이다. 그기준점이 바로 ebp레지스터인 것이다.

 

디어셈블리 모드에서

Dump of assembler code for function main:
0x8048440 <main>:           push   %ebp              //이전 ebp값을 스택에 저장한다
0x8048441 <main+1>:        mov    %esp,%ebp        //ebp에 esp 를 대입했다. ebp=esp
0x8048443 <main+3>:        sub    $0x18,%esp       //esp를 0x18(16진수) 만큼 감소 시켜서 스택의 크기를 증가 시킴
0x8048446 <main+6>:        movl   $0x12345678,0xfffffffc(%ebp)
//0xfffffffc int형으로 -4이고 (%ebp) 가 있으므로 ebp에서 -4가된 주소에 0x12345678을 대입
0x804844d <main+13>:      movl   $0xabcdefba,0xfffffff8(%ebp)
//마찬가지로 ebp에서 -8의 주소에 0xabcdefba 대입
0x8048454 <main+20>:      add    $0xfffffff8,%esp   //esp를 -8
0x8048457 <main+23>:      push   $0x8048440
0x804845c <main+28>:      push   $0x8048728       //printf의 “%08X\n”이 있는주소 push
0x8048461 <main+33>:      call      0x8048328 <printf>
0x8048466 <main+38>:      add    $0x10,%esp       //인자를 넘겨주기 위해 썼던 스택공간 반환
0x8048469 <main+41>:     add    $0xfffffff8,%esp
0x804846c <main+44>:     mov     0xfffffff8(%ebp),%eax
0x804846f <main+47>:      push    %eax
0x8048470 <main+48>:      mov    0xfffffffc(%ebp),%eax
0x8048473 <main+51>:      push   %eax
0x8048474 <main+52>:      call      0x804849c <test>
0x8048479 <main+57>:      add    $0x10,%esp
0x804847c <main+60>:      xor      %eax,%eax
0x804847e <main+62>:      jmp    0x8048480 <main+64>
0x8048480 <main+64>:      leav
0x8048481 <main+65>:      ret
 End of assembler dump.

 

 

<main+3>에서 보면 esp를 0x18 만큼 시킨 것을 볼 수 있다. Main의 스택영역을 잡은 것이다.

 

0x8048441 <main+1>:     mov    %esp,%ebp

에서보면 리눅스는 VC++6.0 과는 다르게 앞의 esp 가 뒤의 ebp에 들어간다.

 

프레임 정보를 확인하자(info frame)

Stack level 0, frame at 0xbffffa28:
eip = 0x8048440 in main (main.c:4); saved eip 0x8048371
source language c.
Arglist at 0xbffffa28, args:
Locals at 0xbffffa28, Previous frame's sp is 0x0
Saved registers:
ebp at 0xbffffa28, eip at 0xbffffa2c

 

 

test 함수로 점프한 후 register , frame

eax               0x12345678         305419896
ecx               0x13                    19
edx               0x8048740           134514496
ebx               0x40148ec0         1075089088
esp               0xbffff9a4            0xbffff9a4
ebp               0xbffff9bc            0xbffff9bc
esi                0x40012280         1073816192
edi                0xbffffa54            -1073743276
eip                0x80484a2           0x80484a2

 

 

Stack level 0, frame at 0xbffff9bc:
eip = 0x80484a2 in test (main.c:21); saved eip 0x8048479
called by frame at 0xbffff9ec

source language c.

Arglist at 0xbffff9bc, args: a=305419896, b=-1412567110

Locals at 0xbffff9bc, Previous frame's sp is 0x0

Saved registers

ebp at 0xbffff9bc, eip at 0xbffff9c0


 

 

위를 보면 saved eip가 보이는데 0x8048479가 스택의 0xbffff9d0 에 저장 되어있다

 gdb03_d.png

<그림 1-3>

0x8048474 <main+52>:  call      0x804849c <test>
0x8048479 <main+57>:  add    $0x10,%esp

 

call을 하면서 test함수로 점프하는데 점프하기전에 eip를 기록해놓고 test함수가 끝나면 돌아오게 된다.

 

이제 test의 디어셈블리모드를 보자

Dump of assembler code for function test:
0x804849c <test>:       push   %ebp
0x804849d <test+1>:    mov    %esp,%ebp
0x804849f <test+3>:      sub    $0x18,%esp
0x80484a2 <test+6>:      movl   $0x11223344,0xfffffffc(%ebp)
0x80484a9 <test+13>:   lea      0xfffffffc(%ebp),%eax
0x80484af <test+19>:     addl     $0x8,0xfffffff8(%ebp)
0x80484b3 <test+23>:   mov    0xfffffff8(%ebp),%eax
0x80484b6 <test+26>:   movl   $0x8048484,(%eax)
0x80484bc <test+32>:   add    $0xfffffff8,%esp
0x80484bf <test+35>:    push     $0x64
0x80484c1 <test+37>:     lea      0xfffffffc(%ebp),%eax
0x80484c4 <test+40>:     push    %eax
0x80484c5 <test+41>:     call      0x80484d0 <PrintHexaNAscii>
0x80484ca <test+46>:     add    $0x10,%esp
0x80484cd <test+49>:   leave
0x80484ce <test+50>:     ret
End of assembler dump.

 

main 함수 에서와 마찬가지 이므로 설명은 생략한다.

 

test함수의 리턴주소를 바꾸기 위해

int *p  = &C;
p = p+2;

*p = test2;

 

P가 가리키는 주소가 C의 주소이므로 C의 주소는0xbffff9c8 이다. 여기서 2(바이트) 를 더하면

0xbffffd0 가 되므로 이 주소는 test의 리턴값, eip값이 저장된 곳이다.

여기에 test2의 주소를 넣었으므로 test 함수는 리턴을 정상적으로 하지않고 test2로 가게 된다.

test2는 정상적으로 call 된 함수가 아니므로  Segmentation fault 가 뜬다.

 

 gdb04_d.png

<그림 1-4>

eip값이 저장된 곳의 값이 원래의 0x8048479 에서 0x8048484(test2주소) 로 바뀌었다.

 

끝까지 실행한 후 bt명령어(스택 트레이스)로 전체 스택 프레임을 확인하고 어떤 함수에서 호출시에 문제가 발생하는지 확인한다. 단 일반적인 라이브러리에서는 오류발생 확률이 없다고 보고, 그함수를 호출시에 문제를 의심한다. 다시 프레임을 이동하면서, 로컬변수와 전역변수 등을 확인하면서 디버깅이 가능하다.

 

 gdb05.png

 

메모리의주소(코드영역)를 엑세스할수 없다는 메세지가 뜬다. 주소가 0xabcdefba 인걸로 보아

P의 주소인데, p의 값을 바꿔주게 되면 오류를 피해 갈수 있을 것 이다.

 

0x8048474 <main+52>:  call      0x804849c <test>
0x8048479 <main+57>:  add    $0x10,%esp
0x804847c <main+60>:    xor      %eax,%eax
0x804847e <main+62>:  jmp    0x8048480 <main+64>
0x8048480 <main+64>:  leave
0x8048481 <main+65>:  ret
End of assembler dump.

오류 없이 종료시키기 위해서

test함수에서

p=p+1;
*p= 0x8048480;

 

이라는 라인을 넣었다. 라인을 넣으니 오류 메시지의 주소가 0x12345678을 따라가고 엑세스 불가능 하였다. 12345678값은 p+3에 있으니 여기의 주소에 값을 main 함수의 call 밑부분의 주소로 넣었다. 0x8048480 <main+64>:  leave

디어셈블리 에서 확인할수 있는데 일단 이 주소로 test2를 돌아오게 하였다.

실행할때는 Segmentation fault 없이 종료 되었는데, 디버그를 해보면

 gdb06.png

 

코드 6이 발생하면서 종료된 것을 알 수 있다.

 

무언가 정상 종료가 아닌거 같다.

그래서 다른 주소로 와보면

0x804847c <main+60>:    xor      %eax,%eax

여기의 주소로 와보면

*p= 0x804847c;

 

 gdb07.png

정상적으로 프로그램이 종료 되었다.