printf 포맷스트링
%s 문자열
%c 문자1개
%f 실수 (double, float) ex) %0.2f => 12.12 소숫점 2자리까지 표기
%u 부호 없는 정수
%d 부호 있는 정수 (2진 보수 방식 등)
%x 16진수 소문자 표기
%X 16진수 대문자 표기
printf("|%20c|\n", 'A');
=> 20칸에 맞추어 문자 A를 출력
printf("|%10.2f|\n", 13.540000);
=> 10칸에 맞추고 소숫점은 2자리까지 출력. 13.54
signed와 unsigned
signed : 양수와 정수 모두 사용 가능 ( -128 ~ 127 까지)
unsigned : 양수만 사용 가능 ( 0 ~ 255까지. 음수를 넣으면 쓰레기값이 셋팅된다고 함)
복습) 어셈블러 종류
- GAS : 리눅스, 유닉스 환경
- NASM : x86 환경 아키텍처에서 사용되는 오픈소스
- MASM : 마이크로소프트사 개발, 윈도우 환경에서 주로 사용
인텔 문법 사용하여 컴파일
=> gcc -masm=intel ~
(32bit) intel과 at&t 문법의 기본 구조 차이
vi 명령어
:w newName.txt
=> newName.txt로 새로운 파일을 만들어 저장한다.
:r !ls %t
k
shift + j
=> 커서 아랫쪽 줄에 현재 파일의 이름을 불러온다. k로 위로 올라간다. j로 밑줄을 끌어온다.
:qa
=> 모든 창 종료
:r sample.s
=> sample.s의 내용을 불러들인다.
PUSH 명령어
- att : pushl 피연산자
- intel : push 피연산자
- 피연산자를 스택에 저장한다
- 스택에 값이 push되면 ESP 값이 x86일 때 32bit(4byte) 감소한다. (스택은 높은 주소에서 시작하여 낮은 주소로 향하기 때문)
- 스택에 관련된 레지스터 (32bit)
1) EBP : 스택의 바닥이자 시작점 (가장 높은 주소이며 함수가 시작되면 종료 전까지 고정된다)
2) ESP : 현재의 스택 위치 (프로세스 실행중에 자료가 저장되면 계속 낮아진다)
3) EIP : 현재 실행중인 명령어의 주소를 저장하고 있다
+ at&t 문법에서는 레지스터 앞에 %가 붙고 즉시값에는 $가 붙는다.
(즉시값 : 어셈블리어 명령어에서 직접 사용하는 상수 값)
ex) %EBP, $1
(gdb) disp $esp
disp $ebp
1: $esp = (void *) 0xffffd448
2: $ebp = (void *) 0xffffd448
(gdb) ni
0x08048482 in main ()
1: $esp = (void *) 0xffffd444
2: $ebp = (void *) 0xffffd448
...
1: $esp = (void *) 0xffffd440
2: $ebp = (void *) 0xffffd448
...
1: $esp = (void *) 0xffffd44c
2: $ebp = (void *) 0x0
(함수 종료)
→ ebp는 고정이고 esp만 변경되었다
( disp 말고 i r $ebp $esp 로 출력해도 된다 )
(gdb) x/i $eip
=> 0x8048484 <main+7>: push $0x3
(gdb) ni
(gdb) x/i $eip
=> 0x8048486 <main+9>: push $0x4
→ 현재 실행중인 명령어($eip) 정보를 출력해준다. ni를 실행하면 계속 변동되는 걸 볼 수 있다.
4byte씩 증감되므로 16진수 표기로 0, 4, 8, c가 반복된다. ( 0 ~ f )
(gdb) ni * 4번 반복하여 push 1 2 3 4 실행된 상태
(gdb) x/5xw $esp
0xffffd438: 0x00000004 0x00000003 0x00000002 0x00000001
0xffffd448: 0x00000000
→ $esp 레지스터에 1 2 3 4 push된 것이 보인다
(gdb) ni
(gdb) x/5xw $esp
0xffffd434: 0x00000005 0x00000004 0x00000003 0x00000002
0xffffd444: 0x00000001
→ 한 번 더 ni 하자 push 5 되었고 $esp 레지스터에 메모리 주소가 보인다
(gdb) x/5xw $esp
→ x로 메모리 조사를 한다. 5개를 출력한다. x 16진수로, w word 4byte 단위로 $esp 레지스터 메모리를 조사함.
$ESP 레지스터를 기준으로 메모리를 조사한 이유
: 낮은 주소부터 보기 위해서
실습) $EBP 기준으로 메모리 조사
(gdb) x/5xw $ebp
0xffffd448: 0x00000000 0xf7e34f36 0x00000001 0xffffd4d4
0xffffd458: 0xffffd4dc
(gdb) x/5xw $ebp -20
0xffffd434: 0x00000005 0x00000004 0x00000003 0x00000002
0xffffd444: 0x00000001
→ -20을 해 준 이유는 4Byte * 5개를 보기 위해서이다. $EBP는 가장 높은 주소(시작점)이기 때문에, $EBP에서부터 메모리를 조사하면 프로그램의 시작점을 넘어가게 된다.
← x/5xw $ebp → x/5xw $ebp -20
-------------------+-------+--+--+--+--+--+-----
High | EBP | 1 | 2 | 3 | 4 | 5 | Low
-------------------+-------+--+--+--+--+--+-----
이 때 $EBP 0xffffd434의 34는 0x00000005의 어느 부분을 의미할까?
→ 정답은 05 이다. 뒤에서부터 값이 채워지기 때문
( 가장 낮은 바이트LSB를 가장 낮은 메모리 주소에 저장하는 리틀 앤디안Little Endian 방식 )
x86 & x86_64 : 리틀 엔디안 방식 사용
ARM : 리틀 엔디안, 빅 엔디안 모두 지원. 리틀 엔디안을 더 일반적으로 사용한다
실습) 메모리에 값을 저장하고 주소를 확인하기 (32bit)
10진수 형식: set *(int *)메모리주소 = 값
16진수 형식: set *(int *)메모리주소 = "0x" + 값
(gdb) set *(int *)$ebp = 12345678
(gdb) x/5xw $ebp
0xffffd448: 0x12345678 0xf7e34f36 0x00000001 0xffffd4d4
0xffffd458: 0xffffd4dc
(gdb) x/1xb $ebp
0xffffd448: 0x78
(gdb) x/1xb $ebp+1
0xffffd449: 0x56
(gdb) x/1xb $ebp+2
0xffffd44a: 0x34
(gdb) x/1xb $ebp+3
0xffffd44b: 0x12
→ byte 단위(b)로 끊어서 표기하고 있다.
$ebp를 기준으로 +1 하면 0x12345678 에서 0x12345678 으로 이동
$ebp를 기준으로 +2 하면 0x12345678 에서 0x12345678 으로 이동
$ebp를 기준으로 +3 하면 0x12345678 에서 0x12345678 으로 이동
(gdb) x/1xb 0xffffd448
0xffffd448: 0x78
→ $ebp의 주소를 기준으로 메모리 조사를 해보면 78이 나온다.
(gdb) x/1xb $ebp+4
0xffffd44c: 0x36
(gdb) x/1xb $ebp+5
0xffffd44d: 0x4f
(gdb) x/1xb $ebp+6
0xffffd44e: 0xe3
(gdb) x/1xb $ebp+7
0xffffd44f: 0xf7
→ 메모리 주소 0x f7 e3 4f 36가 끝에서부터 두 칸씩 묶어(x16진수) 출력되고 있다.
* 0x12345678 출력을 기준으로 볼 때
(gdb) x/4xb $ebp
0xffffd448: 0x78 0x56 0x34 0x12
(gdb) x/4xw $ebp
0xffffd448: 0x12345678 0xf7e34f36 0x00000001 0xffffd4d4
→ byte (b) 단위로 보면 낮은주소(78)부터 높은주소(12)를 출력
→ word (w) 단위로 보면 높은주소(12)부터 낮은주소(78)를 출력 (한번에 출력하면 낮은주소가 뒤에 표기되기 때문)
MOV 명령어 : 데이터를 이동(복사) 하는 명령어
- att : movl [제1피연산자], [제2피연산자]
- intel : mov [제2피연산자], [제1피연산자]
- 제1피연산자의 값을 제2피연산자에 복사한다.
- 피연산자에는 레지스터, 값, 메모리가 올 수 있다.
단, 메모리를 메모리로 복사할 수는 없고, att에서는 제1피연산자에만, intel에서는 제2피연산자에만 값이 올 수 있다
movl $1, %ebp
→ $ebp는 스택의 시작점 주소를 담고 있으므로 Segmentation fault 에러가 발생
(알고보니 at&t 문법으로 작성된 코드를 intel로 컴파일하고 있었다... 참고할것)
(gdb) disas /r main 에서 발췌
=> 0x08048480 <+3>: 68 78 56 34 12 push $0x12345678
(gdb) i r $eip
eip 0x8048480 0x8048480 <main+3>
(gdb) x/i $eip
=> 0x8048480 <main+3>: push $0x12345678
→ eip 레지스터에 0x12345678을 넣는 명령어를 실행하고 있다.
(gdb) i r $eax
eax 0xf7fbe248 -134487480
→ 원래 $eax 레지스터에는 쓰레기값이 들어가 있었다
(gdb) disas /r main
=> 0x08048487 <+10>: b8 01 00 00 00 movl $0x1,%eax
(gdb) i r $eax
eax 0x1 1
→ 실행후 1이 셋팅되었음
AX(16bit) : AH(8bit) + AL(8bit)
EAX(32bit) : AX + AX
RAX(64bit) : EAX + EAX
RAX (64) | |||||||
EAX (32) | EAX (32) | ||||||
AX (16) | AX (16) | AX (16) | AX (16) | ||||
AH | AL | AH | AL | AH | AL | AH | AL |
AH
- AX 레지스터의 상위 8비트. 32 bit 환경에서 사용
AL
- AX 레지스터의 하위 8비트. 32 bit 환경에서 사용
AX
- EAX 레지스터의 하위 16비트. 32 bit 환경에서 사용
EAX
- RAX 레지스터의 하위 32비트. 32 bit 환경에서 사용
RAX (default)
- 64비트 레지스터, x86_64 아키텍처 환경에서 사용된다.
* movl이 아니라 movb를 사용하면 8비트 데이터 이동을 할 수 있다
movw 16비트
movl 32비트
movq 64비트
%esp 레지스터의 경우, 괄호로 감싸주지 않으면 Segmentation falut 에러가 발생한다.
(gdb) x/xw $esp
0xffffd444: 0x12345678
(gdb) disas /r main
=> 0x08048485 <+8>: c6 04 24 01 movb $0x1,(%esp)
(gdb) ni
1: $esp = (void *) 0xffffd444
2: $ebp = (void *) 0xffffd448
(gdb) x/xw $esp
0xffffd444: 0x12345601
→ 원래 78이었는데, $esp 메모리 주소에 01이 들어감 확인. 왜 끝에 들어갔냐면, 리틀 엔디안 방식을 따라 가장 낮은 주소부터 채워지기 때문이다.
(gdb) disas /r main
=> 0x08048489 <+12>: 66 c7 04 24 56 78 movw $0x7856,(%esp)
(gdb) ni
1: $esp = (void *) 0xffffd444
2: $ebp = (void *) 0xffffd448
(gdb) x/xw $esp
0xffffd444: 0x12347856
→ 역시 $esp 메모리 주소에 7856이 들어감을 확인할 수 있다.