카테고리 없음

Assembly 단의 Low Level 환경 및 빅/리틀 엔디안

수낭 2022. 4. 19. 12:50

1. 환경 설정

SASM 검색

Windows용 다운로드

 

64비트 체계에 맞게 64비트 선택

 

Create new Project 클릭

 

기본적으로 위와 같은 내용이 만들어진다.

 

PRINT_STRING은 어셈블리어는 아니고,

SASM에서 제공하는 유틸리티 함수이다.

 

Ctrl+S를 눌러서, ‘HelloWorld’라는 이름으로 저장해주자

 

좌측은 F9 (Build and Run) 버튼이고,

우측은 F5 (Debug) 버튼이다.

F5 버튼으로 실행해보자.

 

Hello World가 잘 출력된 것을 확인

 

save .exe를 눌러서, 윈도우 실행 파일로 저장

 

test라는 이름으로 저장하자.

 

클릭해도 아무런 반응이 없다.

(Hello World만 출력하고 바로 종료되기 때문)

 

이동하고자 하는 파일 디렉토리에서

cmd. 을 입력하고 Enter를 눌러주면, cmd로 간편하게 이동된다.

 

 

어셈블러컴파일러와 유사하게, 번역기의 역할을 한다.

(SASM이라는 통합 환경 자체가 번역기라고 생각하면 된다. - 번역기+문서편집기)

 

section이라는 것은 무엇일까? 왜 써주는 걸까?

 

우리가 만든 윈도우 실행 파일(.exe 확장자 파일)은 좌측 <File>의 구조로 만들어져 있다.

.text라는 Section에는 코드들이 들어갈 것이고,

.data라는 Section에는 ‘Hello World’같은 고정된 값이 들어갈 것이다.

 

- 이해를 돕기 위해, 컴퓨터 구조를 한 번 살펴보자.

- 컴퓨터에는 다양한 부품들이 존재하지만,

CPU, 메인 메모리, 하드디스크가 핵심이라고 할 수 있다.

- 메인 메모리휘발성이지만, 하드디스크(SSD, 등)은 영구적이다.

- 메인 메모리는 비교적 CPU와 가깝지만, 하드디스크는 매우 멀다.

 

LOL 게임으로 를 들어보자.

1. LOL을 설치 (하드디스크에 설치)
2. LOL을 실행 (하드디스크 -> 메인 메모리로 일부 적재)
3. LOL을 실행시킨 이후 (하드디스크 <-> 메인 메모리 왔다갔다)

 

이 사진처럼, File -> Memory로 이동하게 된다. 

(하드디스크 -> 메인 메모리)

 

사실상 CPU – 레지스터 – 메인 메모리

세 경로로 왔다~갔다 하면서, 작업하게 된다.

(어셈블리어의 경우 위와 같지만, C++로 넘어가면 레지스터는 고려 안 해도 된다.)

(어셈블리어의 경우, 가장 Low한 언어이다.)

 

오늘 배운 내용들은 단순 상식이므로, 꼭 외우지는 않아도 된다.



2. 데이터 기초

 

가장 핵심이 되는 3총사는 [CPU, 레지스터, 메모리] 이다.

CPU는 주로 CPU 내에 있는 ALU를 뜻한다.

 

컴퓨터는 데이터를 저장할 때, 0과 1이라는 비트로만 저장한다.

 

계산기를 프로그래머용으로 설정해서 띄워보자.

 

위의 버튼(비트 전환 키 패드)를 클릭해서,

64개의 전구들을 이용해서 실습해보자.

 

QWORD(4word), DWORD(2word), WORD(2byte), BYTE로 변환해서 사용할 수 있다.

 

각 자리의 숫자를 눌러서

1로 바꿔가며 2진법으로 표현할 수 있다.

 

음수를 표현하기 위해서는, 최상위 비트를 사용하면 된다.

즉, 최상위 비트가 음/양 표시를 위해 사용되므로

BYTE, 즉, 8비트여도, 최대 값은 2^7(0-127)이다.

 

주로 ‘음수’를 표현하기 위해서

2의 보수 체계를 사용하는데,

2의 보수 체계는 전 비트를 뒤집고 +1을 해주면 되고,

1의 보수 체계는 전 비트를 뒤집기만 한다.

 

예시 1) 1111 1111 (1)은 0000 0001 (1)을 뒤집은 뒤,

+1을 더해준 값이므로, (1111 1110 + 0000 0001)

-1을 의미하게 된다.

 

예시 2) 0000 0111 (7)에서 최상위 비트를 1로 set하면,

1000 0111(-121)이 되는 이유는, 7에서 -128을 해서 이다.

(최상위 비트는 -128이라고 생각하면 편하다.)



3. 레지스터 기초

 

8 bit = 1 byte
16 bit = 2 byte = 1 word
32 bit = 4 byte = 2 word = 1dword (double-word)
64 bit = 8 byte = 4 word = 2dword = 1 qword (quad-word)

 

(Register가 아닌, Register Set으로 표현되었다.) -> (비행기 조종실 느낌)

(여기서 메모리RAM을 의미한다.)

 

레지스터가 정말 중요한 역할을 한다.

(C++에서는 레지스터의 역할을 잘 모르는 사람이 태반이다.)

 

하드디스크는 너무 멀고, 메인 메모리는 비교적 가깝지만, 역시나 거리가 있는데

레지스터는 아예 CPU 내부에 위치해 있기에, 빠르게 접근이 가능하다.

 

CPU 명령어를 보면, 주로 레지스터를 사용하는 경우를 자주 볼 수 있는데,

레지스터는 주로, 연산에 사용된 임시 결과를 저장하는 용도이다.

 

레지스터의 종류는 다양해서,

rax, rbx, rcx … 등 여러 개를 사용할 수 있다. (a, b, c …)

 

레지스터는 웬만하면 64 bit로 구성되어 있기에, 보통은 그렇게 생각하면 되는데,

rax는 전면적으로 64 bit를 full로 사용하고 싶을 때,

eax는 절반인 32 bit만 사용하고 싶을 때,

ax는 그 절반인 16 bit만을 사용할 때, 등으로 나눠질 수 있다.

ah, al은 ax를 반반 나눈 상위 8 bit와 하위 8 bit를 의미한다.

우선 mov 명령어를 알아보자.

mov reg1, cst

mov reg1, reg2와 같이 사용하게 되면,

우측의 값에서 -> 좌측의 값으로 데이터를 넣어준다. 라는 명령이다.

 

mov cl, 0xffffffff

위와 같이 짠 경우,

cl은 ax의 하위 1byte, 즉, 8 bit만 저장이 가능한데,

범위를 초과하는 값인 0xffffffff를 넣어주면 어떻게 될까?

 

상단의 망치 버튼 (Ctrl + F9)를 누르면, 실행없이 빌드만 하게 되는데,

어떤 문제가 발생하는지 알아보자.

(에러가 있는지만 체크하는 용도)

 

Build log:
[16:09:14] Build started...
[16:09:14] Built successfully.
C:\Users\nr978\AppData\Local\Temp\SASM\program.asm:9: 
warning: byte value exceeds bounds

byte 값이 범위를 초과했다는 에러 메시지를 볼 수 있다.

 

mov eax, 0x1234
mov rbx, 0x12345678
mov cl, 0xff

0xffffffff를 0xff로 범위에 맞게 조절한 뒤,

(16진수 두 개를 합치면, 1 byte가 된다.)

 

[16:11:32] Build started...
[16:11:32] Built successfully.

다시 빌드만 해보면, 성공적으로 실행된 것을 볼 수 있다.

 

디버그 모드를 누르면,

 

다 실행되지 않고, 첫번째 줄에서 멈추는 것을 볼 수 있다.

이후, f10 혹은 f11을 누르면, 순차적으로 다음 행으로 이동시킬 수 있다.

(둘의 차이점은 나중에 알아보자.)

 

Debug에 진입한 상태에서,

Debug -> Show registers와 memory를 클릭하면, 

메모리와 레지스터에 대한 정보를 볼 수 있다.

 

이 위치에서, f10을 눌러서 해당 줄을 진행시키면,

 

rax에 0x1234라는 Hex 값이 저장된 것을 볼 수 있다.

  • EAX에 넣어줬으니, RAX의 절반이 1234로 채워진 것

(우리가 레지스터에 원하는 값을 넣어준 것이다.)

 

결과적으로, 모든 값이 잘 저장된 것을 확인



mov al, 0x00
mov rax, rdx

이번에는 위 코드가 어떻게 동작하는지 알아보자.

 

원하는 위치에 Break Point를 잡고,

실행시킨 뒤, F5를 한번 더 눌러주면, Break Point로 바로 이동시킬 수 있다.

 

이제 F10을 눌러서, 해당 줄을 실행시켜보자.

 

eax를 반반 나눈 뒤, 하위 8 bit를 al이라고 칭했었는데,

al0x00이라는 값을 넣어줬으므로, eax 레지스터의 값이 0x1234에서 0x1200이 된 것이다.

(레지스터는 ’a’ 라는 레지스터 이므로, rax나 eax나 같은 의미이다. -> 범위만 차이가 있다.)

 

mov rax, rdx는 무리없이, 잘 실행된다. (설명할 것이 없음)



4. 변수와 레지스터

 

메모리와 레지스터의 상호작용 과정을 알아보자.

.exe 파일의 구조를 보면, text, data 등의 다양한 메모리 저장을 위한
영역들이 따로 존재한다는 것을 전에 배웠었다.

이 .exe파일을 실행하게 되면, 파일의 내용들이 위 그림처럼 메모리적재되는 원리이다.

 

메모리의 구조를 보면, 첫번째 그림의 내용 외에도,

온갖 정보들이 같이 할당이 된다고 볼 수 있는데,

이 중, 쉽게 사용할 수 있는, DataBSS 영역을 사용해서 실습해보자.

 

section .data

data 영역에 대해 알아보자.

- 위 코드는, 변수의 선언 및 사용이라고 볼 수 있다.

- 변수는 단순히 데이터를 저장하는 바구니이다.

(레지스터는 CPU에 들어있는 조그맣지만 빠른 바구니)

- 처음 바구니를 사용할 때, 사용한다고 선언해야 한다. -> (이름과 크기를 지정)

1) 초기화 된 데이터
 2) [변수이름] [크기] [초기값]
 3) [크기]는 정해진 값들을 써줘야 한다. -> db(1 byte), dw(2 byte), dd(4 byte), dq(8 byte)

(16진수 2개를 합치면, 1 byte이다.)

 

section .bss

bss는 data와 사용 방법은 비슷하지만,

초기화 되지 않은 데이터를 넣어주게 된다.

1) 초기화 되지 않은 데이터
 2) [변수이름] [크기] [개수]
 3) [크기]는 정해진 값들을 써줘야 한다. -> resb(1 byte), resw(2 byte), resd(4 byte), resq(8 byte)

 

section .data
a db 0x11
b dw 0x2222
c dd 0x33333333
d dq 0x4444444444444444

section .bss
e resb 10

결과적으로, 위와 같이 작성한 뒤, 망치(build)를 해보니, 잘 작동한다.

 

굳이 data 영역과 bss 영역을 구분한 이유는,

과거에는 메모리가 비쌌기 때문에, 최대한 용량을 줄이려는 노력에서 비롯되었다.

(bss는 초기화 되지 않은 데이터를 사용하기에, 따로 영역 지정을 안 해도 되므로,) -> 2회차 이해 함.. 아마..

 

이제 어떻게 작동되는지 살펴보자.

F5를 눌러서 실행시키자.

(첫번째 줄에서 멈춘다.)

 

전에 사용했던 Register 창의 하단에서 Memory로 토글버튼을 누른 뒤,

Memory 창만 밖으로 빼서 사용하는 것이 편리하다.

 

Variable or expression : 살펴볼 변수 이름
Value : 해당 값
Type : Hex = 16진수로 표현
b : byte = byte 단위로 표현
개수 : 해당 데이터 기준 (byte), 몇 개의 데이터를 보여줄지

 

개수를 비워두면, 해당 값만 출력되기에, 

a의 값인 0x11만 출력되는 것을 볼 수 있지만,

 

12개로 늘리게 되면, a, b, c, d가 순차적으로 메모리를 차지하고 있으므로,

a의 다음 주소들을 갖고있는, b, c, d 또한 Value에서 확인이 가능하다.

 

그렇다면, section .data 영역의 a라는 데이터를

레지스터에 넣기 위해서는 어떻게 해야할까?

 

mov rax, a

section .data
a db 0x11

위처럼 작성하게 되면, a라는 값의 주소가 rax에 들어가게 된다.

(우리가 원하는 방식이 아니다.)

 

왜 그렇게 되는지 확인해보자.

 

mov rax, a 구문에 break point를 걸고, 실행 -> F5 -> F10을 해보면,

해당 줄이 실행된 직후로 이동할 수 있다.

 

그 결과, rax에 알수없는 값이 들어있는 것을 볼 수 있다.

이 값은 a의 주소값인데, 이를 확인하기 위해, 메모리 창을 사용해보자.

 

똑같이 Hex, byte로 설정하고, 이번에는 Address 옵션을 체크하게 되면,

해당 값을 주소로 갖는 곳을 읽어오게 된다. (포인터 개념)

그 결과, 0x11이 출력되는데, 이는 a db 0x11 와 같이, 우리가 a에 넣어뒀던 값이다.

 

그리고 a뿐만 아니라, b, c, d 등의 주소를 보기 위해서는,

a의 주소인 0x403010에 +1을 해주면 b의 주소를 볼 수 있다. 

(순차적으로 메모리를 갖기 때문에)

(0x11, 0x22는 모두 16진수 2개로 이루어진 값들이고, 16진수 2개가 합쳐지면 1바이트이므로,)

 

mov rax, [a]

a의 주소값을 넘기는 것이 아니라, a에 담긴 값을 넘기고 싶다면,

mov rax, [a]와 같이 사용하면 된다.

(C++의 * 포인터와 비슷하다.)



a의 값은 0x11로 1 byte 뿐이지만, 

rax 기준으로 8 byte를 꺼내지기 때문에, b, c, d의 값까지 rax에 넣은 결과이다.

(mov al, [a]와 같이, 1 byte만 저장하도록 할 수도 있다.)

 

물론 반대로도 가능하다.

이번에는 레지스터 값과, 상수값을 -> 메모리(a)에 넣어보자.

 

mov [a], 0x55

하지만, 위처럼 작성하면 에러가 발생한다.

 

1.  mov [a], byte 0x55
2.  mov [a], word 0x6666
3.  mov [a], cl

위처럼, 크기에 맞게 지정해줘야 한다.

(0x55를 byte만큼만 [a]에 넣는다.) -> 0x55는 0ㅌ0000 0055와 같이, 0이 생략된 형태이다.

cl의 경우, 어차피 1 byte만을 의미하는 레지스터이므로,

크기를 지정해주지 않아도 괜찮다.

 

Break Point를 사용해서, F10으로 하나씩 진행시켜서 살펴보자.

 

1번의 경우, a에 0x55라는 1 byte 크기의 값이 잘 들어가고,

 

2번의 경우, a는 1 byte지만, 그 크기를 초과해서 b의 범위를 사용하여,

0x6666이라는 word(2 byte) 크기의 값을 저장한다.

 

rcx가 현재 0xff이고, c 레지스터의 최하값은 역시 0xff이므로,

 

3번의 경우, a에 0xff라는 1 byte 값이 저장된다.

 

이번 시간에 살펴본, a, b, c와 같은 메모리 이름들은,

가상의 내용들이고, 실제로 메모리에 저장할 때는 주소로만 사용된다.

(모든 데이터들이 각자의 주소를 갖고 있다.)



5. 문자와 엔디안

 

데이터는 보는 방식에 따라서 달라지지만,

데이터 자체는 절대 변하지 않는다.

(Hex, Dec(int), Bin 등에 따라, 0xff, 7890, 1011로)

즉, 같은 데이터를 어떻게 분석/해석 하느냐에 따라 달라진다.

 

예를 들어, 0x11 = 17 = 0o21 = 0b00010001이다.

(각각 16진수, 10진수, 8진수, 2진수)

 

a db 0x11, 0x11, 0x11, 0x11

위와 같이 작성하면, a의 주소부터 순차적으로 0x11이라는 1 byte 크기의 16진수 값을

4개 붙여서 넣는다. (배열 형태)

 

msg db 'Hello World', 0x00

 

 

위처럼, 알 수 없는 값이 들어가 있다.

이는 아스키코드 (ASCII)로 변환된 ‘Hello World’에 해당되는 문자들이다.

 

마지막에 0x00을 넣어줬던 것을 볼 수 있는데,

이는 문자열의 끝을 표시하기 위함이다.

 

@

위 사진은 아스키코드 표이다.

0x48은 ‘H’ 이고, 0x65는 ‘e’이며, 0x0은 NULL인 것을 알 수 있다.

 

‘Hello World’를 ASCII로 표현한 것이, 위 드래그 영역인 것이 되는데,

이부분을 복사한 뒤, 

 


PRINT_STRING msg
xor rax, rax
ret

section .data
msg db 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x0

그대로 붙여넣는다.

 

똑같이 ‘Hello World’가 잘 출력되는 것을 확인

 

PRINT_STRINGSASM에서 지원해주는 API이고,

어셈블리에서 지원해주는 것이 아니다.

 

[엔디안]

1. 리틀엔디안과 2. 빅엔디안이 있다.

빅엔디안작은 -> 큰 순서대로 간다. (기본)

리틀엔디안큰 -> 작은 순서로 간다. (거꾸로)

 

section .data
b dd 0x12345678

위와 같이, dword로 0x12345678의 값을 넣고,

메모리 창에서 확인해보면 아래와 같은 결과가 나온다.

 

즉, SASM은 리틀엔디안을 사용하는 것을 알 수 있다.

대부분의 데스크탑 환경은 리틀엔디안으로 저장된다.

(Intel이나 AMD가 리틀엔디안 방식이기 때문에)

 

그렇다면, 왜 굳이 리틀엔디안을 쓸까?

장점) 캐스팅에 유리하다. (1 byte만 긁어오려 할 때, 끝까지 보지 않아도, 0x78을 가장 먼저 찾음)
단점) 숫자 비교에 불리하다. (가장 큰 숫자가, 가장 마지막에 있으므로)



6. 사칙연산

 

 

 

input창에 입력을 받고 싶을 때, 사용한다.

 

우리는 input을 받아서 (키보드) 초기값을 세팅할 것이므로,

굳이 초기값을 세팅할 필요가 없다. 그래서 bss 영역을 사용한다.

 

%include "io64.inc"

section .text
global CMAIN
CMAIN:
mov rbp, rsp; for correct debugging

GET_DEC 1, al ; 키보드로부터 1byte 입력 받아서, al (레지스터)에 입력
GET_DEC 1, num ; 키보드로부터 1byte 입력 받아서, num (메모리)에 입력

PRINT_DEC 1, al
NEWLINE
PRINT_DEC 1, num

xor rax, rax
ret

;section .data

section .bss
num resb 1

위와 같이 INPUT에 1, 2를 입력하고, F9로 실행해보면,

 

OUTPUT이 1, 2가 출력되는 것을 볼 수 있다.

 

[더하기]

ADD a, b 
(a= a+b)
a는 레지스터 or 메모리
b는 레지스터 or 메모리 or 상수
- 단! a, b 모두 메모리는 X

(1) 레지스터 + 상수
GET_DEC 1, al
add al, 1 ; 레지스터 + 상수
PRINT_DEC 1, al ; 1+1=2

(2) 레지스터 + 메모리
add al, [num]
PRINT_DEC 1, al
NEWLINE

(3) 레지스터 + 레지스터
mov bl, 3 ; bl = 3
add al, bl ; al += bl
PRINT_DEC 1, al
NEWLINE

(4) 메모리 + 상수
add [num], 1 ; 에러
section .bss
    num resb 1
  • 상수를 메모리에 더해줄 때는, 상수의 크기지정해줘야 한다.
add [num], byte
(1 바이트를 더해준다.)
bss 영역에 resb 1 크기의 num을 생성했더라도, 이는 그저 메모리 주소이므로,
직접 지정해줘야 한다.
(그저 주소만을 알고있기 때문 -> 크기를 넘으면 다른 주소 침범 사용)

(5) 메모리 + 레지스터
add [num], al ; al은 1 byte인 것을 알고 있으므로 지정 안 해도 된다.
PRINT_DEC 1, [num]
NEWLINE

(6) 메모리 + 메모리 (불가능)Error Occured
add [num], [num]

[12:20:43] Warning! Errors have occurred in the build:
C:\Users\nr978\AppData\Local\Temp\SASM\program.asm:15: error: invalid combination of opcode and operands
gcc.exe: error: C:\Users\nr978\AppData\Local\Temp\SASM\program.o: No such file or directory
[12:20:43] Before debugging you need to build the program.

 

num2라는 값을 입력받고, 실행한 결과는 위와 같다.

(section .bss / num resb 1)

 

[빼기]더하기와 거의 같다.

sub a, b (a -= b)
이외의 연산은, 더하기와 완전 동일하다.

 

[곱하기]어셈블리 코딩할 때, 별로 등장하지 않는다. (몰라도 된다.)

mul reg명
- mul bl => al * bl (mul에 (1) 지정된 reg와, (2) al 레지스터를 연산에 사용하게 된다.)
-- 연산 결과를 ax에 저장

- mul bx => ax * bx
-- 연산 결과는 dx(상위 16비트) ax (하위 16비트)에 저장
- mul ebx => eax * ebx 
(안중요하니 생략)

Ex) 5 * 8 은?
mov ax, 0 ; 0으로 초기화
mov al, 5
mov bl, 8
mul bl ; al * bl (5 * 8) = 40
PRINT_DEC 2, ax
NEWLINE



[나누기]어셈블리 코딩할 때, 별로 등장하지 않는다. (몰라도 된다.)

div reg명
- div bl => ax / bl – ax를 기본적으로 사용한다.
-- 연산 결과는 al(몫), ah(나머지)를 저장

Ex) 100 / 3 은?
mov ax, 100
mov bl, 3
div bl
PRINT_DEC 1, al – 몫(al) 출력
NEWLINE
PRINT_DEC 1, ah ; 에러 – ah는 PRINT_DEC로 출력이 불가능하므로, al에 옮겨서 출력 권장

mov al, ah
PRINT_DEC 1, al – 나머지(ah) 출력



결과 : 100 / 3 = 33 * 3 + 1



7. 시프트 연산과 논리 연산

(1) 시프트 연산

BYTE로 맞추고,

비트 시프트를, 산술 시프트로 맞춰주자. 

(보통 산술 시프트에 관심을 갖는다.)

 

HEX 값에 32를 입력하고 (16진수 32)

<<를 눌러서 32lsh (left shift) 상태에서 1을 눌러주면,

32를 왼쪽으로 1칸 옮긴다는 명령이 된다.

 

64가 결과값으로 출력된다.

 

=>

0x32에서 0x64로 값이 2배 증가했다.

 

산술시프트의 경우, 최상위 비트변하지 않는다. (1이면 1, 0이면 0) = (부호 유지)

논리시프트의 경우, 최상위 비트도 같이 한다.

 

mov eax, 0x12345678
PRINT_HEX 4, eax ; eax는 4 byte 이므로
NEWLINE
shl eax, 8 ; Shift Left8비트 (1바이트)이동시킨다, (1바이트 = 16진수 2개)


PRINT_HEX 4, eax
NEWLINE
shr eax, 8 ; Shift Right8비트 (1바이트)이동시킨다, (1바이트 = 16진수 2개)


PRINT_HEX 4, eax
NEWLINE



shl을 한 뒤, shr로 다시 되돌렸지만, 1과 2는 돌아오지 않은 것을 확인

위 내용을, 직접 테스트 해보자. (계산기 사용)

 

DWORD로 설정하고, HEX 값을 1234 5678을 입력 (eax 값 테스트)

 

Lsh 8의 결과

 

Rsh 8의 결과

 

그렇다면, 시프트 연산 하는걸까?

 

[1. 곱셈 / 나눗셈]

2의 배수를 곱하거나 나눌 때, 

시프트 연산을 사용하면, 굉장히 빠르게 처리할 수 있다.

 

[2. 게임서버에서 ObjectID를 만들어줄 때]

특정 자릿수의 값을 이용해서

ID를 만들거나 할 때 사용하기도 한다.

ex. 1, 2, 4, 8 자리의 2진수를 몬스터/사용자를 구별하는 코드라고 할 때,

이 코드를 이용해서 ID를 생성하려면, 해당 자리수의 값(ex.1101)을

상위 비트로 쭉 밀어서 1101 xxxx xxxx … 와 같이 만들면, 쉽게 ID를 만들 수 있다.

 

(2) 논리 연산

 

 

숫자끼리의 AND, NOT 연산은 위와 같다.

비트마다 하나의 의미를 부여해주는 경우가 있는데,
해당 비트가 플레이어의 날 수 있는지 여부를 기록한다고 하면,
AND 연산을 통해서 해당 비트가 1인지 0인지 파악하는 용도 등으로 사용된다.

 

위는 XOR 연산이다.

- 동일한 값으로 택 두번 하면, 자기 자신으로 되돌아오는 특성
- 암호학에서 유용하다 (value xor key) (대칭키 암호)
- 동일한 값 두개를 xor 연산하면, 0이 반환된다. (모든 비트가 같기 때문에)



8. 분기문

 

특정 조건에 따라서 코드 흐름제어하는 것

조건 -> 흐름

 

CMP dst, src (dst가 기준)

비교를 한 결과물Flag Register에 저장

 

JMP [label] 시리즈

JMP : 무조건 jump
JE : JumpEquals 같으면 jump
JNE : JumpNotEquals 다르면 jump
JG : JumpGreater 크면 jump
JGE : JumpGreaterEquals 크거나 같으면 jump
JL : JumpLess 작으면 jump
JLE : JumpLessEquals 작거나 같으면 jump

 

rax와 rbx가 같다면 rcx에 1을 넣고, 다르다면 0을 넣게 된다.

즉, rax와 rbx가 같다면 1, 다르다면 0을 출력하는 어셈블리 코드이다.

 

 

 

je에 중단점을 잡고, F5 -> F5 -> F10을 해보면,

eflagsZF(Zero Flag)가 잡혀있어서 같다는 것을 알게된다.

 

cmp rax, rbx의 결과가 위처럼 eflags에 기록되고,

기록된 eflags에 ZF(제로 플래그)가 있는 경우, 서로 같은 값이라는 뜻이므로,

LABEL_EQUAL로 이동한 뒤, 1을 출력하는 방식이다.

(만약 다르다면, 0이 출력된다.)

 

[연습 문제]

Q. 어떤 숫자(1~100)이 짝수면 1, 홀수면 0을 출력하는 프로그램



9. 반복문

 

내가 혼자서 만들어본 버전은 위와 같다. (잘 출력된다.)

 

(위는 강사님의 코드이다.)

dec ecxsub ecx, 1과 동일한 로직이지만,

조금 더 빠르게 동작한다. (ecx--;)

덧셈에 대해서는 inc ecx와 같이 쓰면 된다. (ecx++;)

 

[연습 문제]

Q. 1에서 100까지의 합을 구하는 프로그램 1+2+3…+100을 구하여라.

내가 짠 코드이다. (잘 작동한다.)

 

강사님이 짠 코드.

 

[loop]

나중에 C++코드를 어셈블리로 까보면, Loop [~] 형태의 코드를 볼 수 있는데,

이 역시 반복문을 나타내는 코드이다.

loop를 사용하면, ecx 레지스터에 미리 넣어둔 값을 사용하여,

100에서 0까지 감소시키면서 사용해준다.

10. 배열과 주소

배열 : 동일한 타입의 데이터들의 묶음

- 배열을 구성하는 각 값을 배열 요소(element)라고 함

- 배열의 위치를 가리키는 숫자를 인덱스(index)라고 함

 

배열을 표현하는 방법은 여러가지가 있는데,

- 1번은 문자열 배열이고,

- 2번은 일일이 값을 나열하는 방법이고,

- 3번은 times 키워드를 사용해서 개수지정해주는 방법이다.

- 추가로, bss 영역은 초기화 되지 않은 데이터를 위한 주소를 개수로 생성 가능하다.

 

Intel 기반 아키텍처는 리틀엔디안 (0x0001이 0x01, 0x00으로 표기된다.) 방식이기에

뒤집혀 있다는 것을 생각하면, 정상적으로 잘 입력되어있다.

(word는 2 bytes 이므로, 2칸씩 차지하고 있다.)

 

type의 단위를 word로 지정하면, 0x1로 헷갈리지 않게 볼 수 있지만,

byte 단위로 봐야, 더욱 정확하게 볼 수 있으므로, byte 단위에 익숙해지자.

 

[주소/포인터 다루기 심화]

section .data
  a db 0x01, 0x02 …
mov rax, a

위 코드를 살펴보자. a의 주소값rax에 들어갔었다.

즉, a는 데이터의 첫 주소를 갖고있다는 것이다. (물론 이 주소값은 프로그램 실행시마다 바뀐다.)

PRINT_HEX 1, [a]를 통해, a의 가장 첫 값1을 출력할 수 있다.

 

과연 1뿐 아니라 뒤에 있는 값들은 어떻게 추출할까?

여기서 배열의 개념을 활용해야 한다.

 

 

위와 같이 a의 주소1씩 추가해서, 모든 원소일일이 출력할 수 있다.

이번에는 지난번에 배웠던 반복문을 활용해서

모든 원소들을 출력해보자.

 

[연습 문제]

Q. a 배열의 모든 데이터를 출력해보자.

 

a ecx를 매 반복마다 더해주는데,

ecx1~4까지 순차적으로 더해지므로,

결과적으로, 0~5번째 원소를 출력해주게 된다.

 

- 그렇다면, 아래와 같이, dword5개 원소는 어떻게 출력할까?

(다소 까다롭다.)

 

word 크기의 데이터가 5개연속적으로 위치하고,

 

b는 리틀엔디안에 의해, 0x1, 0x0 … 과 같이 구성되어 있다.

 

1칸씩만 이동시켜서 에러가 발생 

(출력 값 : 1, 100, 1, 100)

 

2칸씩 (word는 2 byte 이므로) 이동시켰더니 정상 작동 

(출력 값 : 1, 1, 1, 1)

  • PRINT_HEX로 2 byte씩 읽는 것에 주의
section .data
a db (btye) 0x11
b dw (word) 0x2222 -> 혼동 주의
c dd (dword) 0x33333333
d dq (qword) 0x4444444444444444

 

주소를 다룰 때,

[시작주소 + 인덱스 * 크기] 와 같은 패턴이 자주 등장한다.



11. 함수 기초

어셈블리어에서는 함수라는 용어를 잘 쓰지 않고,

보통 프로시저(Procedure) 혹은 서브루틴(Subroutine)이라고 한다.

(C++에서는 함수라고 부르기에, 여기서도 함수라고 하겠다.)

 

call 명령어함수명을 호출하면, 해당 함수로 이동한다.

이전에 살펴본 LABEL유사하지만,

다음 시간에 배울 스택프레임을 배우면, 차이점을 알 수 있게 된다.

(ret 명령어를 만나면, 나를 call했던 부분으로 되돌아간다는 차이점)

 

함수가 있는 코드를 디버깅할 때, 주의할 점이 있는데,

F10과 F11 중에서, 

함수 호출 위치에서, F10을 누르면, 해당 함수 속으로 들어가지 않고,

바로 함수 다음으로 이동한다. (F10을 눌러도, 물론 함수가 실행은 된다.)

만약 함수 호출 위체엇, 해당 함수 속으로 진입해서까지 디버깅하고 싶다면,

F11을 눌러주면 된다.

(F10 : step over)
(F11 : step into)

 

 

위와 같은 이유로,

함수의 매개변수를 위해서는 스택메모리를 사용한다.



12. 스택 메모리 / 스택 프레임

(*중요* : 해킹 쪽을 공부한다면, 기본기에 해당)

 

 

 

 

stack을 사용하는 명령어는 push와 pop이 있다.

 

 

 

rsp의 HEX 값 복붙 후, address 체크

 

 

 

rsp가 e38을 가리켰었는데, 이제 다음 스택 top 위치인 e30을 가리키고 있다.

(지금 어디까지 사용했는지, 커서처럼 알려주게 된다.)

 

1이라는 값이, 0x60fe30이라는 주소값에 push되었다.

 

 

 

 

 

pop 이후에도, 딱히 메모리에서 값을 꺼내거나, 메모리를 없애거나 하지는 않고,

그냥 값을 복사해서 가져오기만 한다.

 

스택 메모리는, 우리가 필요할 때 마다 할당하는 것이 아니라,

처음부터 크게 할당을 해놓고, 우리가 그때마다 쪼개서 조금씩 사용해 나가는 방식이다.

 

11:12부터 스택메모리에 대한 상세 설명. -> 이해 못함

 

rbp라는 레지스터를 사용하는 이유는?

rsp라는 레지스터에 주소값 연산을 해서 call 하거나 return을 하게 되면,
rsp는 말그대로 현재 스택의 top 위치 (cursor와 유사)를 나타내기 떄문에,
언제라도 스택에 추가 데이터가 들어오면, 그 가리키는 위치가 변하게 되므로 코드가 굉장히 어려워질 수 있으므로 적합하지 않다.

하지만 rbp는 정말 스택 상대주소 계산용으로만 존재하기 때문에,
우리가 rbp 값을 변경하지 않는 이상, rbp는 같은 곳을 가리키고 있으므로 더욱 적합하다.

 

 

bp 값에 +16을 해주면 2를

bp 값에 +24를 해주면 1을 구할 수 있다.

 

bp를 관리하는 이 일련의 과정들스택 프레임 이라고 한다.

어떤 종류의 함수호출하는지에 따라서 스택의 공간왔다갔다 한다.

 

스택 프레임에는 다양한 데이터들이 저장된다.

1. 넘겨준 인자
2. 돌아가야 할 곳return 주소
3. 이전 함수사용하던 공간을 알기 위한 bp 값이 있고,
4. 함수 내부적에서 저장용도로 사용할 어떠한 변수들

 

이전 bp 값을 왜 사용하는지 이해 못함 ㅠㅠ

 

 

 

우리가 마음대로 stack에 5, 2를 push했던 것을 복원시켜줘야 한다.

방법 1) 5, 2를 pop 해준다.

방법 2) 5, 2를 push 해서 주소가 8*2 만큼 내려갔으므로,

Stack Pointer, 즉, 현재 스택 top 위치를 가리키는 포인터를 +16 해준다.

 

아직 끝까지 이해 못하고 종료함.

 

해킹쪽을 공부한다면, 더욱 깊게 공부해야 하지만,

그게 아니라 C++을 위한 학습이라면, 이정도면 충분하다.