2011년4월28일..VS6.0디버깅에서 disassmble하여 Stack과 컴퓨터를 알아가자!! (Windows)

 

오늘은 어제 분석하다 말았던 Stack에 대해 자세히 짚고 넘어가자…

또한 컴퓨터에 대해 연구도 해보자!!

어제 리눅스의 vi에디터에서 작성했던 소스를 이번에는 VS6.0에서 디버깅할 것이니…
프로젝트를 생성하고 리눅스공유폴더에서 메모장으로 파일을 열어 복사한다.

 

 

main()의 시작점에 브레이크포인트(F9)을 걸고 F5키를 눌러 디버깅모드로 진입한다.

 

메뉴에서 view – Debug windows – Resister와 Memory를 클릭하여 상기와 같이 보기 좋게 배치를 하고,
Wacth창에 &A, &B, main함수의 주소 (main), test함수의 주소 (test)를 볼 수 있게 입력한다.

disasmssebley도 클릭하여 아래와 같이 C언어가 어셈블리어로 변환된 모습을 본다…

여기서 잘 관찰해야 할 점은 resister창의 EBP, ESP값이다. 기록해 두자…
(정보수집을 끝내고 마지막에 최종 종합정보분석을 할 것이니 기록을 해야 한다. 도식과 표로 정리해 놓을 것이니 참조할 것.)

ESP = 0x0013FF84
EBP = 0x0013FFC0

EIP레지스터의 값은 현재 화살표가 가리키는 메모리의 주소로 앞서 배웠던 메모리구조에서 Code영역에 해당되는 곳을 가리키고 있다.
push ebp명령을 실행하기 전의 상태…

 

EBP의 값이 0x0013FFC0이므로 이 값도 볼 수 있으면서 그 이전의 주소도 확인할 수 있게 메모리창에 주소를 입력하자.
현재 ESP와 EBP를 메모리상에 보기 쉽게 빨간 사각형으로 표시하였다. 확인할 것.

 

을 눌러,
메모리 Code영역 0x0040BE30번지의 push ebp 명령을 실행한 후 결과…
84번지에서 80번지로 올라가서 EBP의 값이 메모리에 저장된 것을 확인하자.

(스크린샷의 프로시저 단위 실행은 VS2010에서 부르는 것이고 VS6.0에서는 Step over라고 한다.)

 

 

mov ebp, esp명령을 실행 후…
ebp의 값이 esp와 같아 진다.

mov는 데이터 이동명령으로 먼저 오는 값에 뒤에 오는 값을 대입한다.
C언어에서와 같이 생각하면,
ebp = esp가 되는 것이다.
자세한 설명은 뒤에 할 것이다.

결과적으로 ebp는 esp와 같아져 C0에서 80까지 끌어 올려진 것이다.

 

 

sub esp, 4Ch명령을 실행한 후…
esp의 값은 0x80 – 0x4C = 0x34가 되어 esp는 13칸(52바이트)만큼 올라간다.

 

 

34번지 까지 올라간 esp에서,
아래에 있는 push명령 세 개를 실행하고 난 후에…(이 부분에 대해 선생님께서 설명을 하지 않으셨다.)

esp는 28번지가 되고 다음 int A = 1; (A에 1을 대입하라는 명령) 를 실행하게 될 것이다.

 

mov dword ptr [ebp-4], 1명령 실행 후…
EBP가 가리키는 80번지의 바로 한 칸 위에..즉, 4Bytes이전 7C번지에 1이 들어간다. (01 00 00 00은 리틀엔디안 저장법)

dword는 double word의 약어로 두 배 크기의 워드를 뜻한다.
워드란…CPU가 한 번에 데이터를 처리하는 단위로 버스의 크기와 동일하다. 그러니 레지스터의 크기와 동일하다.
예전 인텔8086 CPU는 16bit CPU로 word의 크기가 16bit였다. 그 때 정한 기준이 아직 이어지고 있어 인텔 CPU은,
word의 크기가 아직 16bit이다. 80386DX(SX는 완전한 32bit가 아니다.) 부터 32bit CPU시대가 시작되어 현재까지 32bit CPU가 주류를..
이루고 있다. 인텔에서는 32bit CPU를 만들 때 기존 8086과 호환성(이전에 작성된 프로그램도 실행)을 유지하기 위해 가상의 8086을..
만들어 실행될 수 있게 하고 16bit레지스터의 확장판을 만들어 Extended를 앞에 붙여 EAX, EBX, ECX… 이렇게 부르기로 하였다.
8086에서 쓰던 AX는 EAX 안에 있다. (누산기를 두 개 만든 것은 아니다.) 이 부분이 잘 못 됨.
Blog2Book시리즈 중 멀티코어CPU이야기를 보니 프로그래머가 취급하는 레지스터는 수가 적으나,
실제 레지스터의 갯수는 매우 많다고 한다. 즉, 실제 회로는 AX와 EAX가 따로 되어 있을 수 있다. (확신은 없음.)
나의 포스트 중 잘 못된 부분이 많이 있으나 발견 즉시 수정할 순 없으나 공지를 하고 조금씩 수정할 예정...
- 2011년9월16일 수정함.

다음은 ptr에 대해…
C언어에서 포인터개념이라 보면 된다. 뒤에 [ebp – 4] 의 주소를 참조하여 그 주소에 찾아가 1이라는 값을 넣으라는 뜻이다.
C언어로 바꾸면,

* (0x0013FF80 – 4) = 1;      =>     *(p - 4) = 1;

그런데 여기서 궁금한 점이 예전에 어셈블리어를 공부하며 배운 mov R0, @R1   (R은 레지스터를 뜻하고 문법은 신경 쓰지 말 것.)
전송명령에서 @는 R1레지스터의 값이 가리키는 주소의 값을 읽어와 R0레지스터에 넣는다고 배웠는데,
이것도 포인터가 아닌가?   mov R0, @R1 + 4 이렇게 쓸 수도 있다. 음…혼란스럽군…

 

mov dword ptr [ebp-8], 2명령 실행 후…
EBP가 가리키는 80번지의 두 칸 위에..즉, 8Bytes이전 78번지에 2이 들어간다. (02 00 00 00은 리틀엔디안 저장법)

A와 B의 경우를 살펴보니 모두 EBP를 기준으로 상대적인 위치로 변수가 위치한다.
어셈블러에선 변수A, B라고 하지 않고 메모리의 주소가 이름인 셈이니 즉, 다음과 같이 symbol table을 그려보면…

type name address 어셈블(지역변수)
int A 0x0013FF7C [ebp – 4] 0x00000001 (1h)
int B 0x0013FF78 [ebp – 8] 0x00000002 (2h)

C언어에서 int A; 는 C문법일 뿐이고 실제 CPU와 메모리를 보면 상기 symbol table의 주소에 32bit binary값이 들어가는 것이다.
그럼 직접주소를 지정하여 0x0013FF7C에 바로 1이라는 값을 넣지 왜 ebp의 상대적인 위치를 지정하여 넣는가?

26일에 배운 메모리구조가 그렇게 지역변수를 stack이라는 영역에 넣어서 그렇고 stack이라는 영역은 LIFO구조이고 이런 구조가 컴퓨터시스템 구성에 효율적이라 그러나…

자세한 데이터 이동방법들은 나중에 배운다. ㅎㅎㅎ

 

잠시 Code영역에 들어간 값들(명령코드)을 살펴보자…

 

다시 처음의 main()의 시작으로 돌아가…
0x0040BE30번지에 push ebp라는 명령어의 코드를 보고 싶다.
메모리창에 주소를 입력하고 확인해보니 0x55이다. 0x0040BE30과 다음 명령의 위치인 0x0040BE31의 차이가 1이므로 1바이트이다.

 

0x0040BE31번지에 mov ebp, esp라는 명령어의 코드는,
0x0040BE31번지 ~ 0x0040BE32번지에 있구나.. 0x8B 0xEC 구나…

mov명령과 ebp레지스터, esp레지스터..이렇게 세 개가 있는데 2Byte밖에 안 되는군.
그건 그렇게 만들었다 치고 메모리에 들어간 코드는 Big Endian일까? Little Endian일까? 
x86명령어를 찾아보면 나올 것이다. (굿보이가 어떤 책을 찾아 보라 했는데 VS6.0어셈블러였나…)

 



0x0040BE33번지에 sub esp, 4Ch라는 명령어의 코드는,
0x0040BE33번지 ~ 0x0040BE35번지에 있구나.. 0x83 0xEC 0x4C 구나…

이건 sub, esp, 4Ch 이렇게 세가지 코드가 있군.
끝에 0x4C는 4Ch (h는 16진수를 뜻하는 어셈블리어)와 일치하니까..
앞서 적은 빅엔디안일 확률이 높아졌다. 그래도 아직 안심할 순 없어!!

 

 

다시 돌아와 STACK영역에 대해 깊이 알아 보자…

rep stos dword ptr [edi]를 실행하기 전에,
ebp에서 19칸 위에(76바이트 전) 34번지와 ebp의 바로 위인 7C번지를 자세히 살펴보자.

 

 

rep stos dword ptr [edi] 명령이 실행된 후에 7C번지 부터 34번지까지 모든 값들이 어디서 많이 본 값들로 채워져 있다.
예전 VS6.0으로 디버깅을 처음 할 때 보았던 변수의 쓰레기값이 아닌가? 신기하군.
이런 명령을 사용하여 한 번에 같은 값으로 초기화를 시켰구나…

선생님께서 설명하다 지나친 부분이 Code영역 0x0040BE36번지부터 38번지까지 세 개의 push를 계산하지 않고 강의를 하셔서…
main()의 stack영역이 12Bytes적게 나왔다.
이 정도는 디버깅을 돌려보고 결과를 확인하면 쉽게 알 수 있으니 크게 문제될 것은 없고,
아직 EBX, ESI, EDI레지스터를 설명할 단계가 아니니 지나치는 것이 더 좋을 듯 하다. (혼란스러워짐. 뇌의 용량 오버플로)

 

 

test()에 진입하기 전에 다시 main()의 stack을 확인하자…
0x0013FF80 ~ 0x0013FF28의 범위를 갖고 있군.

 

push 3명령을 실행하니,
28번지 위인 24번지에 3이 들어간다.

 

다음에 push 4명령을 내리니,
24번지 이전의 20번지에 4가 들어간다.

중간정리를 해보면,

상기의 그림과 같이 메모리의 stack영역의 구조를 보기 쉽게 적고 다음 명령을 실행해 보자.
너무 길어지면 기록하기 힘드니 중간중간 도식을 추가하는 것이 좋다.

계속…

0x0040BEA0번지의 call @ILT+10(_test) (0040100f) 명령을 실행하면,
call명령이 다음과 같은 동작을 하여,
ⓐpush eip   (다음에 실행할 명령이 들어 있는 Code영역의 주소를 stack에 밀어 넣는다.)
ⓑeip = &test (eip에 test()의 주소를 넣는다. 문법은 따지지 말 것.)

즉, esp가 push명령에 의해 -4감소되고 그 자리에 main()로 복귀할 주소인 0x0040BEA5를 넣는다.
복귀할 주소는 아래의 스크린샷에,


다음에 수행될 명령인 add esp, 8이 있다.
0x0040BEA5번지가 복귀주소이니 test()가 끝나고 main()으로 복귀할 때 add esp, 8을 실행할 것이다.


F11키를 눌러 함수내부로 들어간 것이 jmp test가 있는 곳이다.

 

 

test() 내부로 들어갔다.
ILT+10에서 jump하여 test로 가는 것은 후에 분석하기로 하고 일단 들어와서 stack의 변화를 먼저 알아보자.

 

 

push ebp 명령을 실행하여 stack에 현재 ebp의 값(main() stack의 하한)을 넣는다.
후에 mov ebp, esp명령을 실행하여 ebp의 값을 esp값까지 올리는 일을 하나 main에서 했으므로 다음 결과만 확인하자.

 

 

변수C선언 전 stack의 모습은 상기의 스크린샷과 같다.
보기 좋게 파란사각형으로 main :: A,B  인자 A,B를 표시하고,
보라색사각형으로 리턴주소와 main : ebp를 표시했다.
0x0013FEC0 ~ 0x0013FF14번지까지 test에서 사용하기 위한 지역변수를 초기화했다.
그럼 0x0013FF14번지에 변수C가 놓일 것이다. 확인해 보자.

 

int C = 5;
mov dword ptr [ebp – 4], 5를 실행하고 난 뒤에,

예상대로 0x0013FF14번지에 5가 들어갔다.
(ebp값이 0x0013FF18번지니 4Bytes를 빼면 0x0013FF14번지.)
esp값을 확인해 보니 0x0013FEBC다…초과해서 급하게 그림판으로 그려 넣었다.

 

int D = 6;
mov dword ptr [ebp – 8], 6를 실행하고 난 뒤에 10번지에 6이 들어간다.

 

 

아쉽게도 이 소스코드는

*i = (int)test2; 라는 코드를 main()의 return전에 추가하여 test()를 끝내고 main()으로 복귀할 주소에 test2()의 시작주소를 덮어 써서 return까지 진행할 수 없다.

0x0040C04E ~ 0x0040C051까지 세 개의 코드가 main()으로 복귀하는 코드이다.

main :: ebp의 값이 들어 있음.

ⓐ mov esp, ebp  (esp에 ebp값을 넣어서 esp를 내림, 원상복귀)
ⓑ pop ebp        (main :: ebp로 내림)
ⓒ ret

ret는 pop eip와 같은 동작을 한다.

main()으로 복귀하는 주소

ⓐ eip = *esp   (esp에는 main()함수로 복귀할 주소가 들어 있다.)
ⓑ esp = esp + 4 (stack pointer 내림)

 

0x0040BEA5로 복귀하여 esp의값에 8을 더하여 esp에 저장하면 esp는 두 칸 아래의 main() stack의 상한에 위치하게 된다.
이렇게 인자 A와 B는 제거된다.

 

 

지금까지 수집한 정보들로 종합적인 분석을 하여 결론을 내리면,

 

개발자가 C언어로 프로그램을 작성하여 실행파일을 만든 뒤에 실행을 하면 메모리의 Code영역에 어셈블리어 코드가 들어가고,
Data, BSS영역은 아직 전역변수를 하지 않았고 선언도 하지 않았으니 제외하고,
Heap은 동적할당이라 이것도 아직 하지 않았다. 제외하고,
Stack영역에 선언한 변수들이 들어가게 된다는 것을 이 번 실험을 통해 알게 되었고 Stack을 이해하고 나아가 CPU와 메모리를 이해할 수 있다고 생각한다.

스택구조의 맨 밑에 커널3는 내가 마음대로 이름을 붙인 것이 99.9999% 잘 못 되었을 확률이 있다.
인입함수의 전 단계에 무엇이 있는지 모르겠으나 아마 운영체제가 아닌가 하여 커널이라 이름을 붙인 것이다.

이 구조를 통해 알 수 있는 사실은 인입함수가 main()을 호출하고 main()은 test를 호출할 때,
자신이 사용하던 밥상(변수영역)의 위에 호출한 다음의 명령이 위치한 주소를 쌓고,
다음에 자신의 밥상 받침의(init :: stack의 하한) 주소를 쌓는다.
그 다음 주소부턴 main :: stack의 시작이고,
차례로 선언된 변수가 놓인다.
main()에서 또 다른 함수를 호출할 때 인자가 있으면 인자를 CLEDC법으로 뒤에 오는 변수를 먼저 쌓고,
main()으로 돌아올 주소를 쌓는다.
다음 main :: stack의 주소를 쌓고,
다음에 test내부에서 선언한 변수들을 순서대로 쌓는다.

 

 

                        결  론                        

함수 호출 시 메모리를 확보하고 함수가 끝나고 복귀할 때 메모리를 반납한다.
함수를 호출하면 스택에 계속 쌓여 많이 호출하면 스택이 오버플로우되어 심각한 문제를 일으킬 수 있으니 적당히 호출할 것.

Code영역과 Stack영역의 위치가 뒤 바뀌어져 있다. 다음과 같이 Code가 위로 가야 하는데 밑에 있다.

 

아직 리눅스 GNU Debugger를 사용해서 정보를 수집하고 분석하지 않아 섣부른 결론을 내리기 어려우나…

상기의 그림은 linux의 메모리구조가 아닌가 한다.
아니면 그림에 있는 번지가 잘 못 된 것일지도…
내일 학교에 가서 컴퓨터 구조책을 다시 봐야겠다. 오늘은 이만 꿈나라로~  -_-zZ zZZ

울보

 

 

 

 

노트필기와 칠판 영상등을 올립니다.
문서 작성 완료 전 참조할 것.