디스어셈블리 코드로 살펴보는 점프문과 함수 포인터
디스어셈블리 코드를 통해 점프문인 continue, break 명령과 함수 포인터에 대해 분석한다
1. “continue” 와 “break”
C/C++ 에서 continue와 break 명령문들은 어떻게 동작할까?
다음은 continue와 break를 다룬 아주 간단한 C/C++ 코드이다.
다음은 이에 대한 디스어셈블리 코드이다.
위의 디스어셈블리 코드들 중에서 중요한 부분만 따로 간추려 최대한 간단하게 설명해 보도록 하겠다.
1
2
3
4
5
6
7
8
9
if(cnt == 1) continue;
00AB144D cmp dword ptr [cnt],1
00AB1451 jne main+45h (0AB1455h)
00AB1453 jmp main+2Eh (0AB143Eh)
if(cnt == 3) break;
00AB1455 cmp dword ptr [cnt],3
00AB1459 jne main+4Dh (0AB145Dh)
00AB145B jmp main+58h (0AB1468h)
먼저 “continue” 부분을 살펴 보자.
이미 알고 있는 것 처럼, 프로세서는 조건문이 만족되어 상태 플래그가 참이되면 실행 순서를 주소 “0AB143Eh”로 분기 시킨다.
주소 “0AB143Eh”는 명령어 “cnt++”의 주소이다.
“break” 부분도 위와 유사하다. 조건문이 만족되어 상태 플래그가 참이되면 프로그램은 “0AB1468h”로 분기 되어 루프문을 탈출 한다.
추가로 “goto”문에 대해서도 약간 다루어 보겠다.
1
2
3
4
5
6
7
8
9
Lable:
num++;
00381468 mov eax,dword ptr [num]
0038146B add eax,1
0038146E mov dword ptr [num],eax
goto Label;
00381471 jmp Lable (381468h)
보는 바와 같이 MSVC 컴파일러는 “goto”문을 “jmp”으로 바꿔 처리한다.
물론 “goto”를 사용하려면 레이블을 추가적으로 사용해야 하는 불편함이 따른다.
여기서 우리는 “goto”문이 “continue”문이나 “break”문과 같이 “jmp”로 컴파일 된다는 사실을 알 수 있었다.
그러나 “goto”문은 코드를 복잡하게 만들기 때문에 사용하지 않는 것이 좋다.
제대로 꼬인 스파게티 코드를 만들고 싶지 않다면 말이다.
2. 함수가 호출되는 방식
이제 함수 포인터가 내부적으로 어떻게 작동하는지 디스어셈블리 코드를 통해 살펴 볼 차례이다.
C++ 코드
main 함수의 디스어셈블리 코드
점프 테이블 코드
func 함수의 디스어셈블리 코드
지금으로써는 우선 func() 함수가 호출 되는 부분만 살펴 보도록 하자.
1
2
3
4
result = func(10, 20);
003134E5 push 14h
003134E7 push 0Ah
003134E9 call func (31101Eh)
프로세서가 func() 함수 호출 명령을 만났을때 func() 함수의 주소는 “31101Eh”다.
그런데 어째서 호출되는 함수의 주소가 “BD2F60h”가 아니라 “31101Eh”일까?
func()의 주소는 “BD2F60h” 인데도 말이다.
이는 컴파일러에 의해 만들어진 점프 테이블에 대한 맵핑 작업 때문이다.
맵핑 작업에 대해 설명해 보자면,
소스코드가 컴파일 될때 MSVC 컴파일러는 분기를 원활하게 할 수 있도록 코드 영역 앞쪽에 모든 함수의 주소를 수집, 정리하여 점프 테이블을 생성하는데 이 테이블을 이용하여 분기하는 일련의 과정을 맵핑 작업이라고 볼 수 있다.
불행하게도 아직 컴파일러 공부를 제대로 하지 않아서 명확하게 설명하기는 어렵다.
혹여라도 설명이 잘못 되었다면 따끔한 질책을 부탁 드린다.
그럼 컴파일러에 의해 생성된 점프 테이블 코드 부분을 살펴 보자:
빨간색 박스로 감싸진 영역이 보이는가?
각각 위에서 부터 아래 방향으로 “func()” 함수와 “func2()” 함수의 실제 코드 영역에서의 주소이다.
이를 통해 우리는 두 가지 사실을 알 수 있다.
1) 프로세서는 점프 명령을 만나면 점프 테이블을 이용해 프로그램을 분기 시킨다. 2) 점프 테이블은 함수들의 주소를 갖고있다.
이 흐름의 과정을 그림으로 나타내 보면 다음과 같다 :
요컨대, “프로세서가 점프 명령을 만나 분기 작업을 할때 점프 테이블을 이용한다.” 라고 할 수 있겠다.
3. 함수 포인터
그럼 이제 func() 함수에 대한 함수 포인터 부분을 살펴 보도록 하자. 만약 위의 글을 제대로 읽었다면 이번 설명은 무척이나 이해하기 쉬울 것이다.
1
2
3
4
5
6
7
pfunc = func;
000D1B14 mov dword ptr [pfunc],offset func (0D101Eh)
result = pfunc(30, 40);
000D1B1B mov esi,esp
000D1B1D push 28h
000D1B1F push 1Eh
000D1B21 call dword ptr [pfunc]
아시다시피 함수의 이름은 함수의 시작 주소를 의미한다.
배열의 이름이 그 배열의 시작 주소를 의미하는 것과 같다.
위의 코드에서 볼 수 있는 것 처럼, func() 함수의 주소 “0D101Eh”는 함수 포인터 변수 “pfunc”에 할당 되어 진다.
나머지 코드는 위의 함수 설명에서 다룬 것과 유사하다.
다만 이번 처럼 함수 포인터를 사용해 함수를 호출할때에는 점프 테이블을 이용하지 않고 함수 포인터 변수에 들어있는 주소를 직접적으로 이용해 분기 한다.







