스택 영역에 차례대로 저장되는 함수의 호출 정보를 Stack Frame
이라고 한다.
Stack Frame
은 지역 변수, 인수 매개 변수, 함수의 반환 주소값 등과 같이 구성된다.
Stack Frame
은 BP
, SP
레지스터를 사용하여 Stack Frame
에 구성된 스택을 접근한다.
BP
레지스터: 스택프레임의 바닥을 가르킨다. 지역 변수나 인자에 참조할 때 사용된다.
SP
레지스터: 스택의 크기를 조정할 때 사용한다.
이러한 BP
, SP
레지스터를 이용한 함수 프롤로그, 에필로그
가 있다.
함수 프롤로그
:
베이스 포인터를 스택에 저장하고 현재 스택 포인터를 베이스 포인터에 저장한다.
함수 에필로그
:
현재 스택 포인터를 베이스 포인터로 복귀, 베이스 포인터를 복귀한 뒤 처음 호출한 지점으로 돌아간다.
PLT
: 외부 프로시저를 연결해주는 테이블이다.
(프로시저: 함수와 비슷하지만, 함수와 달리 리턴 값을 날리지 않는다.)
PLT
를 통해 다른 라이브러리에 있는 프로시저를 호출 할 수 있다.
GOT
: PLT
가 참조하는 테이블
프로시저들의 주소가 들어있다.
printf()
같은 함수는 우리가 만들거나 구현하지 않아도 쓸 수 있다.
printf()
선언이 들어 있는 stdio.h
를 #include
했기 때문이다.
그러면, printf()
의 구현 코드는 어디에 있는 걸까? printf()
를 구현한 오브젝트 파일에 있을 것이다.
이러한 오브젝트 파일이 모여 있는 곳을 라이브러리 라고 한다.
우리가 이러한 함수들을 사용하는 것은 외부 라이브러리에서 정의되고 만들어진 함수들을 사용하는 것이다.
우리가 만든 코드는 컴파일 후 링킹을 한다.
이 때, 우리가 만든 코드와 라이브러리와 연결을 하는 작업을 링킹이라고 한다.
링킹 방식 중에서 Static Link
방식과 Dynamic Link
방식이 있는데,Dynamic Link
방식이 PLT, GOT를 사용한다.
Static Link
방식:
gcc에 -static
옵션을 줌으로서 사용할 수 있다.
정적 링크방식으로, 실행 파일 안에 라이브러리 동작 코드가 포함된다.
이 때문에, 파일 실행 만으로 라이브러리 함수를 사용할 수 있다.
그러나, 라이브러리 동작 코드가 실행 파일에 포함되면 실행 파일에 크기는 커질 수 밖에 없고, 메모리 관리도 비효율적이게 된다.
Dynamic Link
방식:
gcc에 아무런 옵션이 없으면 기본으로 Dynamic Link
방식이 사용된다.
동적 링크방식으로, 실행 파일 안에 라이브러리 동작 코드가 포함되지 않는다.
해당 프로그램을 실행하기 위한 실행파일 뿐 아니라 라이브러리 파일(.dll .so)도 필수로 있어야 한다.
하지만 라이브러리 내용이 코드에 포함하지 않기 때문에
파일의 크기가 작고 메모리를 보다 더 적게 먹기 때문에 효율적이다.
따라서 공유 라이브러리 함수을 사용하려면
PLT
, GOT
로 라이브러리에 있는 프로시저 주소를 호출해야 한다.
공유 라이브러리 함수 처음 사용 시:
PLT
로 함수 호출 -> GOT
참조 -> 다시 PLT
호출 -> _dll_runtime_resolve
함수를 실행하여 해당하는 함수 주소를 GOT에 저장 -> 해당 함수로 점프
2번째 사용 부터: PLT
로 함수 호출 -> GOT
참조 -> GOT
에 쓰여진 그 함수의 주소가 이미 존재하므로 해당 함수로 점프.
0x00000000004006e7 <+0>: push rbp
0x00000000004006e8 <+1>: mov rbp,rsp
main 함수 프롤로그
0x00000000004006eb <+4>: sub rsp,0x100
rsp에 0x100을 뺌 (160바이트의 메모리 공간을 확보)
0x00000000004006f2 <+11>: mov eax,0x0
eax를 0으로 초기화
0x00000000004006f7 <+16>: call 0x400686 <setup>
setup 함수 call
0x0000000000400686 <+0>: push rbp
0x0000000000400687 <+1>: mov rbp,rsp
setup 함수 프롤로그
0x000000000040068a <+4>: mov rax,QWORD PTR [rip+0x20055f]
0x0000000000400691 <+11>: mov ecx,0x0
0x0000000000400696 <+16>: mov edx,0x2
0x000000000040069b <+21>: mov esi,0x0
0x00000000004006a0 <+26>: mov rdi,rax
0x00000000004006a3 <+29>: call 0x400570 <setvbuf@plt>
각 파라미터에 stdin, 0x0, 0x2, 0x0을 넣고 setvbuf 함수 call
0x00000000004006a8 <+34>: mov rax,QWORD PTR [rip+0x200531]
0x00000000004006af <+41>: mov ecx,0x0
0x00000000004006b4 <+46>: mov edx,0x2
0x00000000004006b9 <+51>: mov esi,0x0
0x00000000004006be <+56>: mov rdi,rax
0x00000000004006c1 <+59>: call 0x400570 <setvbuf@plt>
각 파라미터에 stdin, 0x0, 0x2, 0x0을 넣고 setvbuf 함수 call
0x00000000004006c6 <+64>: mov rax,QWORD PTR [rip+0x200533]
0x00000000004006cd <+71>: mov ecx,0x0
0x00000000004006d2 <+76>: mov edx,0x2
0x00000000004006d7 <+81>: mov esi,0x0
0x00000000004006dc <+86>: mov rdi,rax
0x00000000004006df <+89>: call 0x400570 <setvbuf@plt>
각 파라미터에 stdin, 0x0, 0x2, 0x0을 넣고 setvbuf 함수 call
0x00000000004006e4 <+94>: nop
아무것도 안함(no-operation)
0x00000000004006e5 <+95>: pop rbp
함수 복귀
0x00000000004006e6 <+96>: ret
함수 종료 후, call 다음 명령줄로 이동
0x00000000004006fc <+21>: mov edi,0x400804
"What's your name :" 문자열의 주소를 edi에 대입
0x0000000000400701 <+26>: mov eax,0x0
eax를 0으로 초기화
0x0000000000400706 <+31>: call 0x400540 <printf@plt>
printf call - "What's your name :"이 출력됨
0x000000000040070b <+36>: lea rax,[rbp-0x100]
[rbp-0x100]의 주솟값을 rax에 대입, rax는 gets의 입력값이 된다.
0x0000000000400712 <+43>: mov rdi,rax
rax값을 rdi에 대입
0x0000000000400715 <+46>: mov eax,0x0
eax를 0으로 초기화
0x000000000040071a <+51>: call 0x400560 <gets@plt>
gets 함수 call
0x000000000040071f <+56>: mov edi,0x400818
"Hello, " 문자열의 주소를 edi에 대입
0x0000000000400724 <+61>: mov eax,0x0
eax를 0으로 초기화
0x0000000000400729 <+66>: call 0x400540 <printf@plt>
printf 함수 call - "Hello, "이 출력된다.
0x0000000000400742 <+91>: mov edi,0x400820
"!!!" 문자열의 주소를 edi에 대입
0x0000000000400747 <+96>: call 0x400530 <puts@plt>
puts 함수 call - "!!!" 이 출력된다.
0x000000000040074c <+101>: mov edi,0x400824
"Last Greeting : " 문자열의 주소를 edi에 대입
0x0000000000400751 <+106>: mov eax,0x0
eax를 0으로 초기화
0x0000000000400756 <+111>: call 0x400540 <printf@plt>
printf 함수 call - "Last Greeting : " 이 출력된다.
0x40075b <main+116> lea rax, [rbp - 0x100]
[rbp-0x100]의 주솟값을 rax에 대입, rax는 gets의 입력값이 된다.
0x400765 <main+126> mov eax, 0
rax를 0으로 초기화
0x40076a <main+131> call gets@plt <gets@plt>
gets 함수 call
0x40076f <main+136> mov eax, 0
eax를 0으로 초기화
0x400774 <main+141> leave
0x400775 <main+142> ret
main 함수 에필로그
"filename" 바이너리를 로드한다.
추가) gdb --args ./filename all is well
식으로 프로그램 인자를 줄 수 있다.
함수명의 어셈블리 코드를 본다.
해당 메모리 주소값에 break를 설정한다.
함수 이름을 적어 break를 설정 할 수도 있다.
프로그램을 실행한다.
breakpoint가 없으면 프로그램이 완전히 실행된 후 종료되고,
breakpoint가 있으면 breakpoint 걸린 코드 직전까지 실행되고, 디버깅 값을 보여준다.
공통점:
어셈블리어 한 줄을 실행한다.
다른점:
si - call이 실행되면 call 안의 함수 코드 한줄로 넘어간다.
ni - call이 실행되면 call 함수를 끝마치고 다음 코드 한줄로 넘어간다.
공통점:
소스 한 줄을 실행한다.
다른점:
s - call이 실행되면 call 안의 함수 코드 한줄로 넘어간다.
n - call이 실행되면 call 함수를 끝마치고 다음 코드 한줄로 넘어간다.
레지스터 값을 출력한다.
특정 레지스터 값을 출력한다.
값을 원하는 형식으로 출력해준다.
메모리를 확인한다.
옵션:
o: 8진법
x: 16진법
u: 10진법
t: 2진법 으로 보여준다.
b: 1byte
h: 2byte
w: 4byte
g: 8byte 단위로 보여준다.
i: 역어셈블된 명령어의 명령 메모리를 볼 수 있다.
c: ASCII 표의 바이트를 자동으로 볼 수 있다.
s: 문자 데이터의 전체 문자열을 보여 준다.
그냥 입력하면 프로그램의 memory map을 보여주고, 주소까지 입력하면 그 주소의 대응하는 위치와 정보도 알려준다.
메모리 구역을 정해 주어진 값을 검색해준다.