gdb 할 때 기본적인 disp 항목
disp $esp
disp $ebp
disp $eip
disp /i $eip
disp /4xw $esp
--
실습) 어셈블리 코드를 보고 c언어로 변경하기
어셈블리어만으로는 return이 덧셈연산인지 뺄셈연산인지 알 수 없지 않나 생각했는데
확인해보니 다른 점이 있었다
func1의 return이
a-b일때) sub
mov 0x8(%ebp),%eax
sub 0xc(%ebp),%eax
a+b일때) mov와 add
mov 0x8(%ebp),%edx
mov 0xc(%ebp),%eax
add %edx,%eax
#include <stdio.h>
int func1(int a, int b);
int main(){
int a = 2;
int b = 1;
int c;
c = func1(a, b);
printf("result: %d\n", c);
return 0;
}
int func1(int a, int b){
printf("a: %d, b: %d\n", a, b);
return a-b;
}
// 아래는 a+b
#include <stdio.h>
int func1(int a, int b);
int main(){
int a = 1;
int b = 2;
int c;
c = func1(a, b);
printf("result: %d\n", c);
return 0;
}
int func1(int a, int b){
printf("a: %d, b: %d\n", a, b);
return a+b;
}
func1
0x080484ec <+3>: pushl 0xc(%ebp)
0x080484ef <+6>: pushl 0x8(%ebp)
=> main과 달리 pushl 해주는데 3바이트 떨어진 곳과 2바이트 떨어진 곳이다.
메모리 구조
( main ) ( func1 ~ ~ ~
main ebp int a=2 int b=1 int c b값 a값 복귀주소 func1 ebp = func1 esp
458 454 450 44c 448 444 440 43c ( 여기에 main ebp 주소 저장 )
=> 448 444에 a와 b값이 거꾸로 들어간다는 점을 인지해야한다
func1 ebp b값 a값 문자열
43c 438 434 430
=> push로 다시 b값과 a값 문자열 들어감
main ebp : 0xffffd458 (main 시작!)
(gdb) x/4xw $ebp -8
0xffffd450: 0x00000002 0x00000001 0x00000000 0xf7e34f36
=> 조사해보면 ($ebp -8)에 b값인 1, ($ebp -4)에 2이 있다.
func ebp : 0xffffd43c (func1 시작!)
(gdb) p ($ebp+12)
$1 = (void *) 0xffffd448
(gdb) x/s 0xffffd448
0xffffd448: "\001"
=> 현재 ebp에서 12바이트 떨어진 곳은 448이고 1이 들어있다.
(gdb) p ($ebp+8)
$2 = (void *) 0xffffd444
(gdb) x/s 0xffffd444
0xffffd444: "\002"
=> 현재 ebp에서 8바이트 떨어진 곳은 444이고 2가 들어있다.
즉, pushl 2와 pushl 1이라는 뜻
0x080484f2 <+9>: push $0x80485a8
=> 이것은 printf를 위한 복귀주소
call 0x8048350 <printf@plt>
add $0xc,%esp
=> printf 콜 이후, add로 printf를 위해 할당했던 3바이트 자리를 다시 복구함
mov 0x8(%ebp),%eax
sub 0xc(%ebp),%eax
=> eax 레지스터에 a값인 2 넣고, 거기에 sub로 b값인 1빼면 끝.
leave
과정에서 ebp가 갖고 있던 main ebp 주소를 esp 레지스터로 옮기고
pop을 통해 ebp가 esp 레지스터 값을 전달받아
ebp는 main ebp위치로 이동
pop 되었으므로 esp도 한 바이트 높아졌다
ret
과정에서 eip는 esp가 갖고 있던 복귀주소를 pop으로 가져오고
esp는 한 바이트 높아지며(func1 호출 이후에 실행할 명령어 위치로)
jmp로 eip 레지스터가 call func1 다음줄로 복귀함
add $0x8,%esp
=> 다시 돌아온 메인에서 func1 하느라 할당했던 2바이트 esp 복귀
mov %eax,-0xc(%ebp)
=> mov로 %eax의 값을 %ebp -12 (int c)위치에 저장한다 (func1 return 값)
그 후 pushl로 %ebp -12값과 복귀주소를 저장한후(복귀주소는 printf 안으로 들어갔을때만 확인 가능)
(gdb) x/4xw $esp
0xffffd444: 0x0804859c 0x00000001 0x00000001 0x00000001
문자열 %ebp -12값
(gdb) x/s 0x0804859c
0x804859c: "result: %d\n"
=> 0x0804859c에 문자열이 들어가 있다
add $0x8,%esp
=> printf 종료 후에 다시 esp 복원하고
(gdb) x/4xw $esp
0xffffd44c: 0x00000001 0x00000001 0x00000002 0x00000000
mov $0x0,%eax
leave
ret
=> mov로 eax에 return 0 셋팅후 끝
함수의 스택 프레임SF
함수를 호출할 때 스택에서 생성되는 구조. 실행상태를 저장하고 관리하는 역할
함수가 종료할 때 제거된다
- 인수
- 리턴주소
- 이전 프레임 포인터 : caller(이전 함수의 호출자)
- 지역 변수
(gdb) i r $pc
pc 0x80484b9 0x80484b9 <main>
=> eip 레지스터와 같다
콜링 컨벤션
- 함수를 호출할 때 인수들의 전달 방법과 함수가 반환될 때 스택을 정리하는 규칙
- cdecl, stdcall, fastcall, 64비트 환경에서의 x86_64 system v 등등 ...
주요 콜링 컨벤션
1) cdecl : 주로 c언어에서 사용되고 x86시스템에서 많이 씀
인수 전달 : 인수는 오른쪽에서 왼쪽 순서로 스택에 푸시
스택 정리 : 호출자caller가 스택 정리
( rocky8 에서 이 방식을 사용 )
int a, int b, int c;
printf("문자열", c);
일 때,
rocky8 에서는 3개의 변수공간만 잡는다.
printf call 전에 printf용 인수 공간을 2개 선언한 뒤, 호출이 종료되면 add로 2개 공간을 다시 거둬들인다
centos7 에서는
미리 printf에서 쓸 인수 2개의 공간을 추가로 셈하여 변수 3개 + 인수 2개 = 총 5개의 인수공간을 잡는다
printf가 종료되어도 스택을 정리하지 않는다
2) stdcall : 주로 윈도우 API에서 사용
인수 전달 : 오른쪽에서 왼쪽으로 스택에 푸시
스택 정리 : 호출된 함수callee가 스택을 정리한다
3) fastcall : 함수 호출시 성능을 높이기 위해 사용
인수 전달 : 첫 번째와 두 번째 인수는 레지스터 ECX, EDX로 전달되고 나머지는 스택에 푸시
스택정리 : 호출된 함수callee가 스택을 정리
4) 64비트 콜링 컨벤션 : 윈도우 64비트 환경에서 사용
인수 전달 : 첫 4개 정수 인수는 레지스터 (RCX, RDX, R8, R9)로 전달. 나머지는 스택에 푸시
단, 부동 소수점 ㄷ인수는 XMM0-XMM3 레지스터로 전달.
스택정리 : 호출된 함수 callee가 스택 정리
과제) C언어 소스를 gdb 어셈블리어로 분석하기