변수는 스택에 저장되는데,
높은 주소에서 낮은 주소로 ↓ 채워지게 된다. (LIFO, 선입후출=후입선출)
sizeof(변수)
=> 함수가 아니라 연산자이다. 변수의 크기를 확인.
printf("%c\n", 'A');
=> 'A' 이부분은 리터럴 상수라고 한다. 변수에 변하지 않는 데이터를 넣는것.
char txt[20];
=> 배열. 동일한 자료형으로, 연속된 공간 20개를 메모리에 확보
txt 는 &가 없어도 배열의 시작지점 주소를 반환한다.
리눅스에서 ASLR 주소 랜덤화 기능이 기본적으로 활성화되어 있으므로,
같은 변수를 초기화 하여도 실행할 때마다 주소값이 달라진다.
변수 주소 출력
&aslr: 0xff89aabc
&aslr: 0xffc3341c
&aslr: 0xff8357bc ...
ASLR on/off는 커널 파라미터값으로 제어되는데, /proc/sys/kernel/randomize_va_space
이 값이 0이면 ASLR Off, 1과 2는 ASLR ON
int A = 10;
printf("%d \n", *&A);
=> 10이 출력된다. *는 주소의 값을 출력하고, &A는 A변수의 주소값을 의미하기 때문.
또, 연산순서가 *보다 &이 높기 때문에 * 와 &A로 분리됨.
포인터를 쓰는 이유
call by value => 값만 복사
call by reference => 주소에 직접 접근 가능
주소에 직접 접근하여 계산하기 위해.
포인터 사용
int a = 7;
int *p = &a;
printf("%d \n", *p);
=> a의 값인 7이 출력된다.
*
1) 산술 곱셈연산
2) 인다이렉션(간접참조) 연산자 (ex *&A )
3) 포인터 선언시 사용
echo 0 > /proc/sys/kernel/randomize_va_space
&aslr: 0xffffd4ac
&aslr: 0xffffd4ac
&aslr: 0xffffd4ac
=> ASLR을 OFF 시키자 변수 주소가 고정되었음
( 단, 이것은 임시적인 방법으로 부팅하면 다시 ON 된다 )
영구적으로 ASLR을 OFF 시키는 방법
vi /etc/sysctl.conf
/proc/sys/ 부분을 생략하고 / 슬래시를 . 으로 바꾸어 설정함
reboot하여도 0으로 변동없음
/usr/libexec/gcc/x86_64-redhat-linux/8/cc1 -E aslr_test.c -o aslr_test.i
=> 컴파일러로 직접 -E 옵션을 사용해 .i 파일 생성
/usr/libexec/gcc/x86_64-redhat-linux/8/cc1 aslr_test.i
=> 컴파일러로 aslr_test.o 파일 생성
eip 레지스터
=> 다음에 실행할 명령이 담겨있는 레지스터
(gdb) i r eip
eip 0x80484ee 0x80484ee <main+17>
(gdb) disas main
Dump of assembler code for function main:
0x080484dd <+0>: lea ecx,[esp+0x4]
0x080484e1 <+4>: and esp,0xfffffff0
0x080484e4 <+7>: push DWORD PTR [ecx-0x4]
0x080484e7 <+10>: push ebp
0x080484e8 <+11>: mov ebp,esp
0x080484ea <+13>: push ecx
0x080484eb <+14>: sub esp,0x14
=> 0x080484ee <+17>: mov BYTE PTR [ebp-0x9],0x41
0x080484f2 <+21>: mov WORD PTR [ebp-0xc],0x7e4
... 하략
=> 화살표로 표시된 곳이 next하면 실행될 명령어 위치를 의미함. eip 레지스터와 동일.
( mov 어셈블리어로 저장하라는 뜻 )
해당하는 소스코드 : c = 'A';
0x41이 'A'를 의미함
(gdb) disas /r main
Dump of assembler code for function main:
0x080484dd <+0>: 8d 4c 24 04 lea ecx,[esp+0x4]
0x080484e1 <+4>: 83 e4 f0 and esp,0xfffffff0
0x080484e4 <+7>: ff 71 fc push DWORD PTR [ecx-0x4]
0x080484e7 <+10>: 55 push ebp
0x080484e8 <+11>: 89 e5 mov ebp,esp
0x080484ea <+13>: 51 push ecx
0x080484eb <+14>: 83 ec 14 sub esp,0x14
0x080484ee <+17>: c6 45 f7 41 mov BYTE PTR [ebp-0x9],0x41
=> 0x080484f2 <+21>: 66 c7 45 f4 e4 07 mov WORD PTR [ebp-0xc],0x7e4
0x080484f8 <+27>: c7 45 f0 40 42 0f 00 mov DWORD PTR [ebp-0x10],0xf4240
=> 한 번 next 한 후의 결과. <+0>, <+4>, <+7> 이런 숫자가 의미하는 바는 기계어 코드의 개수이다
0x080484dd <+0>: 8d 4c 24 04
=> 4개 <+4>
0x080484e1 <+4>: 83 e4 f0
=> 3개 <+7>
0x080484e4 <+7>: ff 71 fc
=> 3개 <+10>
0x080484e7 <+10>: 55
=> 1개 <+11>
... 이런식으로 기계어 코드의 개수를 합산하여 표기된 것임
메모리 영역
T(C) D B H S
Text(Code), Data, Bss, Heap, Stack
High Memory - Stack - - Heap - - Bss - - Data - - Text(Code) - Low Memory |
Text 영역에 올라간 데이터 주소가 0x080484dd, 0x080484e1, 0x080484e4 요런것들 ...
구분
주소값 | offset | 기계어코드 | 어셈블리코드 |
0x080484dd <+0>: 8d 4c 24 04 lea ecx,[esp+0x4]
next 한 번 한다고 해서 하나의 라인을 실행하는 것은 아니다.
소스코드는 한 줄이어도, 기계어코드/어셈블리 코드는 몇 줄씩 묶여있을 수 있다
ex)
24 f = 3.14f;
0x080484ff <+34>: fld DWORD PTR ds:0x8048674
0x08048505 <+40>: fstp DWORD PTR [ebp-0x14]
=> 소스코드 한 줄을 실행하는 데에 두 라인이 필요
next와 step 명령어
+ next (n) : 명령어를 실행한 후 다음줄로 이동한다. (함수 내부로 들어가지 않음)
여러 개의 어셈블리 명령어가 묶여 있을 때 한번에 여러개를 실행한다.
+ next instruction (ni) : 명령어를 실행한 후 다음줄로 이동하되, (함수 내부로 들어가지 않음)
여러 개의 어셈블리 명령어가 묶여 있을 때 하나씩 실행한다.
+ step (s) : 다음 줄로 이동한다 (함수 내부로 들어간다)
여러 개의 어셈블리 명령어가 묶여 있을 때 한번에 여러개를 실행한다.
+ step instruction (si) : 다음 줄로 이동한다. (함수 내부로 들어간다)
여러 개의 어셈블리 명령어가 묶여 있을 때 하나씩 실행한다.
(gdb) ni
5: $eip = (void (*)()) 0x8048520 <main+59>
5: $eip = (void (*)()) 0x8048520 <main+63>
5: $eip = (void (*)()) 0x8048520 <main+66>
5: $eip = (void (*)()) 0x8048520 <main+67>
(gdb) n
5: $eip = (void (*)()) 0x8048520 <main+80>
=> ni 하면 한 라인씩, 어셈블리 코드가 조금씩 수행된다.
n 하면 훌쩍 건너뛰어 여러개씩 수행된다.
증감연산자 ++, --
int a = 10;
printf("%d \n", ++a);
printf("%d \n", a++);
=> 11, 11
변수 앞에 붙으면 해당 라인에서 증가된다.
변수 뒤에 붙으면 해당 라인이 수행되고 난 뒤에 증가된다.
list 명령어로 소스코드를 10줄 볼 수 있다. (브레이크 포인트 기준)
이를 set listsize 50 이런식으로 늘릴 수 있는데,
영구적이지 않으므로 gdb에 다시 접속하면 10으로 돌아간다.
EBP 레지스터 : 스택 프레임의 시작주소인 맨 아래쪽(High Memory)을 가리킨다 (함수가 끝날때까지 고정)
ESP 레지스터 : 스택 프레임의 시작주소인 맨 아래쪽(High Memory)에서부터 시작하여, 함수가 실행되면서 이동된다. 다음 명령어가 실행되면서 쌓일 주소, 즉 현재 시점의 가장 맨 꼭대기(Low Memory로 내려감)를 가리킨다
x/16xw $ebp의 경우, 맨 아래쪽을 가리키기 때문에 -16을 해주어야,
16개 전 ~ 맨 아래쪽까지의 주소를 정상적으로 볼 수 있다.
스택은 기본적으로 16바이트로 정렬함
이 정렬을 빼주려면
gcc -m32 -Wall -fno-stack-protector -mpreferred-stack-boundary=2 -g ~
g++ -m32 -Wall -fno-stack-protector -mpreferred-stack-boundary=2 -g ~
컴파일 할 때 이렇게 설정해주어야 함
(gdb) disas main
Dump of assembler code for function main:
0x080484ad <+0>: push ebp
0x080484ae <+1>: mov ebp,esp
0x080484b0 <+3>: sub esp,0x8
=> 0x080484b3 <+6>: mov DWORD PTR [ebp-0x4],0x2
0x080484ba <+13>: add DWORD PTR [ebp-0x4],0x1
0x080484be <+17>: mov eax,DWORD PTR [ebp-0x4]
0x080484c1 <+20>: mov DWORD PTR [ebp-0x8],eax
0x080484c4 <+23>: push DWORD PTR [ebp-0x4]
0x080484c7 <+26>: push DWORD PTR [ebp-0x8]
0x080484ca <+29>: push 0x804856c
0x080484cf <+34>: call 0x8048350 <printf@plt>
0x080484d4 <+39>: add esp,0xc
0x080484d7 <+42>: mov eax,0x0
0x080484dc <+47>: leave
0x080484dd <+48>: ret
End of assembler dump.
=> 정렬 전보다 훨씬 코드양이 줄어든 것이 보인다.
(gdb) n
12 a = ++b; // a = ? b = ?
1: a = 0
2: b = 2
3: $esp = (void *) 0xffffd430
4: $ebp = (void *) 0xffffd438
5: $eip = (void (*)()) 0x80484ba <main+13>
(gdb) p &a
$1 = (int *) 0xffffd430
(gdb) p &b
$2 = (int *) 0xffffd434
=> ebp가 스택 프레임의 시작지점. 438이고, b가 434 a가 430. esp도 430.
4바이트 단위로 주소가 지정되고 있다는 것이 보인다.
426 | 427 | 428 | 429 | |
430 | 431 | 432 | 433 | ← a, esp |
434 | 435 | 436 | 437 | ← b |
438 | ← ebp 위치 |
소스코드: a = ++ b;
0x080484c5 <+24>: add DWORD PTR [ebp-0xc],0x1
0x080484be <+17>: mov eax,DWORD PTR [ebp-0x4]
0x080484c1 <+20>: mov DWORD PTR [ebp-0x8],eax
=> DWORD PTR [ebp-0xc]에 0x1를 add 하는 어셈블리 명령어
=> eax에 [ebp-0x4] ebp가 가리키는 주소로부터 0x4만큼 떨어진 곳을 저장시킨다
=> eax에 저장된 데이터를 [ebp-0x8] ebp가 가리키는 주소로부터 0x8만큼 떨어진 곳에 저장시킨다
램에서 램으로 바로 못 집어넣기 때문에 번거로운 과정을 거침
i r $eax
eax 0x3 3
=> eax 레지스터에는 3이 들어가있다.
* 함수가 끝날 때 return 값은 레지스터 eax에 들어간다 (약속)
어셈블리어 문법
- intel : 윈도우에서 많이 사용
- at&t : 리눅스에서 많이 사용
os에서 서로 섞어서 사용할 수도 있다
intel 문법을 더 많이 사용하지만 둘 다 알아야 함
at&t 문법을 먼저 공부하고 나중에 intel 문법 학습
문법 차이
ADD
at&t
형식 : addl [제 1피연산자], [제 2피연산자]
intel
형식 : add [제 2피연산자], [제 1피연산자]
=> 제 1피연산자와 제 2피연산자를 더한 값을 2피연산자에 저장한다
PUSH
at&t
형식 : pushl [제 1피연산자]
intel
형식 : push [제 1피연산자]
=> 제1 피연산자를 스택에 저장한다. push되면 ESP값이 32bit(4Byte) 쌓이면서 감소한다.
MOV
at&t
형식 : mov [제 1피연산자], [제 2피연산자]
intel
형식 : mov [제 2피연산자], [제 1피연산자]
=> 제 1피연산자에 있는 값을 제 2피연산자에 저장한다
=> 제 1피연산자는 값, 레지스터, 메모리가 들어갈 수 있다. 제 2피연산자에는 값이 들어갈 수 없다.
=> 메모리 -> 메모리 이동은 불가능.
어셈블러 종류
- GAS : 리눅스, 유닉스 환경에서 사용. x86, ARM, MIPS 등 지원
- NASM : x86, x86-64 아키텍처에서 사용되는 오픈소스 어셈블러.
- MASM : 마이크로소프트사에서 개발했고 윈도우 환경에서 사용. 고급 어셈블리 언어 지원
어셈블리어 파일 생성
: n 파일명.s ( GAS )
: n 파일명 .asm ( NASM )
gcc -m32 -masm=intel -S 파일명.c
=> intel 문법으로 컴파일하는 경우
해당 옵션이 없으면 기본값인 at&t 문법으로 컴파일 한다.
=> 여기서 쓸데없는 코드를 제거하면
=> 이렇게 간략하게 정리된다.
=> at&t 문법으로 기본 코드를 만들어 놓은 상태.
CPU 레지스터에 %가 붙는다.값은 $값으로 표시한다.
gcc -m32 -masm=intel -mpreferred-stack-boundary=2 -o sample sample.s
=> 컴파일하여 ./sample로 실행.
( 이외 레지스터들 : eax, ebx, ecx, edx )
컴파일하여 실행해도 printf가 없으므로 나오는 것이 없다
대신 gdb로 확인한다
(gdb) b *0x0804847d
Breakpoint 1 at 0x804847d
=> 브레이크 포인트 설정. b main으로 걸어도 상관은 없음.
(gdb) ni
0x0804848f in main ()
1: $eip = (void (*)()) 0x804848f <main+18>
3: $eax = 1
4: $ebx = 2
5: $ecx = 3
6: $edx = -11052
7: $ebp = (void *) 0xffffd4a8
=> 코드가 없으므로 n으로 하면 한번에 휙 지나가서 안됨
ni로 하나씩 확인하면, 값이 하나씩 들어가는 것을 확인할 수 있다.
과제)
공유한 c언어 소스를 gdb로 분석 해보기