크래프톤 정글/TIL

[크래프톤 정글 5기] week03 알고리즘 주차 열아홉번째 날, CS, 어셈블리어, 레지스터, 오퍼랜드, 메모리 주소, 스택 포인터

양선규 2024. 4. 8. 17:21
728x90
반응형

Byte : 8bit

Word : 16bit

Double Word : 32bit

Quad Word : 64bit

 

CPU16개 범용 레지스터

레지스터의 종류

 

- 각 범용 레지스터의 크기는 64bit이다.

- 16개의 레지스터에 있는, 여러 크기의 하위 바이트 데이터에 대해 연산이 가능하다.

- %rsp : 스택 포인터이며, 런타임 스택의 끝 부분(Top)”을 가리킨다.

 

레지스터의 역사

8086

- 8개의 16비트 레지스터

- %ax ~ %sp

IA32

- 32비트로 확장

- %eax ~ %esp

x86-64

- 기존의 8개의 레지스터가 64비트로 확장 (%rax ~ %rsp)

- 8개의 새로운 레지스터 추가 (%r8 ~ %r15)

 

레지스터는 1, 2, 4, 8 바이트 단위로 사용할 수 있다.

8바이트를 사용하는 경우를 제외하면, 전체 레지스터를 사용하지 않기에 남는 바이트들이 생긴다.

아래는 남는 바이트를 처리하는 방법이다.

- 1 / 2 바이트를 사용하는 경우 : 나머지 바이트 변경 없이 유지

- 4 바이트를 사용하는 경우 : 나머지 상위 4바이트를 0으로 설정한다.

 

 

 

명령어의 구성

명령 코드 : 명령어가 수행할 연산(연산자)

오퍼랜드 : 피연산자

 

오퍼랜드(피연산자)

- 대부분의 인스트럭션은 하나 이상의 오퍼랜드를 가진다

- 연산을 수행할 source , 그 결과를 저장할 목적지 destination의 위치를 명시한다

- source값은 상수로 주어지거나, 레지스터/메모리 주소로부터 값을 읽어온다.

- destination은 레지스터/메모리 주소이며, 해당 주소에 연산 결과가 저장된다.

다양한 메모리 주소지정 방식

 

오퍼랜드의 세 가지 타입

immediate(상수)

- 상수는 ‘$’ 기호 다음에 정수가 오는 형태이다. ex) $-577, $0x1F

- 어셈블러는 해당 값을 인코딩하는 가장 컴팩트한 방법을 자동으로 선택한다.

 

register(레지스터)

- 64bit, 32bit, 16bit, 8bit 레지스터들의 일부분인 8, 4, 2, 1 바이트 중 하나의 레지스터를 가리킨다.

- 위 그림에서 ra는 임의 레지스터a를 나타낸다(레지스터 식별자). 레지스터 값은 R[ra] 형식으로 가져와 사용한다.

- R은 배열이며, 레지스터 식별자를 인덱스로 사용하여 값을 가져온다.

 

메모리 참조

- 유효주소(effective address)라고 부르는 계산된 주소에 의해 메모리 위치에 접근한다.

- Mb[Addr] 형태로 표시, Addr부터 저장된 b바이트를 참조하라는 것.

- 단순화를 위해 일반적으로 아래첨자는 생략한다. ( M[Addr] )

- 메모리 참조를 가능하게 하는 많은 주소지정방식이 존재한다. 가장 일반적인 형태는 표의 마지막에 있는 “Imm(rb, ri, s)” 형태이다.

- 상수 오프셋(주소, 출발지) Imm, 베이스 레지스터 rb, 인덱스 레지스터 ri, 배율 s.

- 배율은 1, 2, 4, 8의 값을 갖고, 베이스/인덱스 레지스터는 모두 64비트 레지스터이다.

- 실제 유효주소(Addr)“Imm + R[rb] + (R[ri] * s)” 로 계산된다.

## 위 그림처럼 오퍼랜드는 다양한 형태가 있으나, 마지막 형태를 제외한 다른 형태 들은 일부가 생략된 특별한 경우이다.

## 배열/구조체를 참조하려면 위 그림보다 복잡한 주소지정방식이 필요하다.

 

 

 

아래는 연습문제이다.

연습문제 3.1

 

왼쪽은 메모리, 오른쪽은 레지스터이다.

메모리와 레지스터를 보고 어떤 값이 담겨있을지 위 문제를 풀어보자.

 

%rax : 0x100

0x104 : 0xAB ( 메모리 주소를 직접 가리켰다 )

$0x108 : 0x108 ( ‘$’가 붙으면 상수로 인식한다. 그 값 자체를 의미함 )

(%rax) : 0xFF ( %rax에 담긴 주소에 해당되는 메모리 값을 의미 )

4(%rax) : 0xAB ( %rax에 담긴 0x100값에, 앞의 상수 4를 더한 주소의 메모리 값 )

9(%rax, %rdx) : 0x11 ( 각 레지스터 값 (0x100 + 0x3) + 앞의 상수 9 = 0x10C 주소의 값)

260(%rcx, %rdx) : 0x13 ( (0x1 + 0x3) + 상수 260(0x104) = 0x108 주소의 값 )

0xFC(,%rcx, 4) : 0xFF ( ( 빈값 + (0x1 * 4) ) + 0xFC(96) = 0x100 주소의 값 )

(%rax, %rdx, 4) : 0x11 ( ( 0x100 + (0x3 * 4)(0xC) ) = 0x10C 주소의 값 )

 

한번 계산해보고 나면 크게 어렵지는 않다는 걸 알 수 있다.

 

 

 

 

데이터 이동 인스트럭션

- 가장 많이 사용되는 인트스럭션은 데이터를 한 위치에서 다른 위치로 복사하는 명령이다.

- 데이터의 이동은 메모리에서 메모리로 한번에 갈 수 없다!!! ex) movq (%rax), (%rsp)

- 대표적인 명령어 : MOV

 

MOV 클래스

- source에서 destination으로 데이터를 어떤 변환도 하지 않고 복사한다.

- movb, movw, movl, movq 4개의 인스트럭션으로 구성된다.

- 각 인스트럭션은 같은 기능을 하지만, 각자 다른 크기의 데이터를 계산한다는 점에서 다르다.

movb : move byte, 1바이트

movw : move word, 2바이트

movl : move double word, 4바이트

movq : move quad word, 8바이트

 

movq 인스트럭션은 32비트 2의 보수 숫자로 나타낼 수 있는 상수 소스 오퍼랜드만을 갖는다. 이후 부호 확장되어 목적지를 위해 64비트 값을 생산한다.

movabsq 인스트럭션은 64비트 상수 값을 소스 오퍼랜드로 가질 수 있으며 목적지로는 레지스터만을 가질 수 있다.

 

작은 소스 값을 더 큰 목적지로 복사할 때 사용하는 명령어들(MOVZ, MOVS)

- 레지스터/메모리에 저장되어 있는 소스로부터 레지스터 목적지로 복사한다.

- 마지막 두 개의 문자는 각각 출발지, 목적지 크기를 나타낸다.

 

MOVZ 클래스 : 목적지의 남은 바이트들을 모두 0으로 채운다(0으로 확장)

movzbw : byteword로 복사

movzbl : bytedouble word로 복사

movzwl : worddouble word로 복사

movzbq : bytequad word로 복사

movzwq : wordquad word로 복사

4바이트 소스를 8바이트 목적지로 확장하는 인스트럭션은 없다. 하지만 이것은 movl로 구현할 수 있다. movlmov클래스 중 예외적으로 남은 상위4바이트를 0으로 채우기 때문이다.

 

MOVS 클래스 : 소스 오퍼랜드의 가장 중요한 비트를 반복해서 복사하는 부호 확장으로 채운다.

movsbw

movsbl

movswl

movsbq

movswq

movslq

cltq : 오퍼랜드가 없다. %eax를 소스로, %rax를 목적지로 사용하여 부호 확장 결과를 만든다. movslq %eax, %rax와 정확히 동일한 효과이지만 인코딩이 좀 더 압축적이다.

 

 

 

 

레지스터의 종류

 

 

위 레지스터들의 크기를 참고하여 아래 문제를 풀어보자.

 

연습문제 3.2

 

오퍼랜드를 보고 적절한 인스트럭션을 선택하는 문제이다. ex) movl, movq 등등..

메모리 주소와 레지스터를 헷갈리지 말자!!! 괄호 여부를 잘 봐야 한다!!!

(%rsp)같은 메모리 주소엔 무슨 값이 있을지 모르므로 옆 오퍼랜드를 기준으로 정해야 한다.

movl %eax, (%rsp)  -> %eax (4byte)를 옮겨야 하므로 l

movw (%rax), %dx  -> %dx(2byte)에 옮겨야 하므로 w

movb $0xFF, %bl -> $0xFF는 상수이므로 %bl(1바이트) 기준으로 선택

movb (%rsp, %rdx, 4), %dl  -> %dl(1바이트)에 옮겨야 하므로 b

movq (%rdx), %rax  -> %rax(8바이트)에 옮겨야 하므로 q

movw %dx, (%rax)  -> %dx(2바이트)를 옮겨야 하므로 w

- 각 레지스터의 크기만 알고 있다면 간단하다. "복사될 레지스터 OR 복사된 값이 입력될 레지스터 크기 기준"으로 b, w, l, q중 선택하면 된다.

- movb $0xFF, %bl의 경우 소스 오퍼랜드는 상수이고 %bl(1byte)에 담아야 하기 때문에  movb이다.

 

아래 문제도 풀어보자.

연습문제 3.3

 

이 명령어들은 에러를 출력한다. 어느 부분이 잘못되었는지 찾으면 된다.

 

movb $0xF, (%ebx) -> (%ebx) 라는건 불가능하다!! 메모리 주소는 64비트로 이루어져 있는데, %ebx 레지스터는 최대 32비트만 저장할 수 있으므로 메모리 주소를 담을 수 없다. 즉 (%ebx) 자체가 모순이다.

movl %rax, (%rsp) -> 옮길 데이터가 8byte 이므로 movq가 사용되어야 함

movw (%raw), 4(%rsp) -> 메모리에서 메모리로 즉시 데이터를 옮기는 건 불가능하다!!

movb %al, %sl -> %sl 이라는 레지스터는 존재하지 않는다.

movq %rax, $0x123 -> %rax의 값을 옮겨야 하는데, 목적 오퍼랜드가 상수이며 데이터 그 자체다. 즉 성립하기 위해선 목적지 오퍼랜드가 레지스터 또는 메모리 주소여야 한다.

movl %eax, %rdx -> 이거 오타다!!! 이건 맞는 명령이고 movl %eax, %dx가 진짜 문제이다.

-> %dx를 기준으로 movw가 사용되어야 한다. 물론 이렇게 하면 32비트 -> 16비트 이므로 상위 16비트 데이터는 손실된다.

movb %si, 8(%rbp) -> %si(2byte)를 옮겨야 하므로 movw가 사용되어야 한다.

 

더 작은 레지스터를 고른다고 생각하면 대체로 맞는다.

 

 

 

 

 

C코드와 어셈블리 코드

 

C언어에서의 포인터는 어셈블리어에서 단순히 메모리 주소를 의미한다.

-> (%rdi) = *xp

포인터를 역참조하는 과정은, 포인터를 레지스터에 복사한 후 이 레지스터를 메모리 참조에 이용하는 것이다.

-> (%rdi)%rax(변수x) 레지스터에 담았다.

-> %rsi(y)(%rdi)메모리에 담았다.

-> 마지막 ret(return)에서 %rax(변수x)를 리턴했다.

-> 메모리 주소를 레지스터 %rax(변수x)에 담아, 레지스터를 메모리 주소로써 이용한 것.

 

또한, %rax(변수x)같은 지역변수들은 메모리에만 저장되는 것이 아닌 종종 레지스터에 저장된다.

레지스터로의 접근은 메모리보다 훨씬 더 빠르기 때문이다.

 

 

 

프로세스 메모리의 형태

프로세스에 할당되는 메모리 형태

 

메모리의 할당

- 메모리는 "프로세스 단위"로 할당되며, 위 그림과 같은 형태로 할당된다.

- 프로세스 단위로 할당되기 때문에 다른 프로세스의 데이터가 들어올 일은 없으며, 없도록 의도되었다.

- 하지만 프로세스 끼리 데이터를 공유할 수 있는 방법이 있는데, IPC 매커니즘을 사용하는 것이다.

- IPC는 메시지 전달, 공유 메모리, 파이프, 소켓 등의 다양한 방법으로 구현된다.

 

stack 영역

- 리턴값, 지역변수, 매개변수 등이 임시로 저장되는 공간

- 프로그램 실행 시 컴파일러 및 인터프리터에 의해 자동으로 할당된다.(정적 할당)

- stack 영역은 push, pop 연산으로 데이터가 들어오고 나가지만, "표준 메모리 주소지정 방법"을 이용해 top에 있는 데이터가 아니더라도 조회할 수 있다.

- ex) movq 8(%rsp), %rdx   -> rsp의 주소에서 8을 추가함으로써, top의 다음 주소에 있는 데이터를 복사해 올 수 있다.

 

heap 영역

- 프로그래머가 필요할 때마다 사용하는 메모리 영역.

- 배열, 문자열 등 객체형태 데이터가 저장되며, malloc() 등의 함수로 메모리를 동적으로 할당할 경우에도 사용된다.

 

data 영역

- 전역 변수, static 변수 등 프로그램이 사용하는 데이터를 저장하는 공간

- 전역 변수 또는 static 값을 참조한 코드는 컴파일이 완료되면 data 영역의 주소값을 가리키도록 바뀐다. 전역변수는 변경될 수 있기에 Read-Write 이다.

 

code 영역

- 사용자가 작성한 프로그램 함수들의 코드가, CPU에서 수행할 수 있는 기계어 명령 형태로 변환되어 저장되는 공간

- 컴파일 시점에 결정되고 코드를 수정할 수 없도록 Read-Only 이다.

 

스택 데이터의 저장과 추출(push, pop / 인스트럭션 : pushq, popq)

- 스택은 후입선출(last in first out) 자료구조

- push로 데이터를 추가하고 pop으로 데이터를 제거한다.

- 제거되는 값은 가장 최근에 추가된 값이며, 그 값은 다른 push연산 등이 수행될 때까지 스택에 여전히 남아 있는다.

- 스택은 배열로 구현될 수 있으며, 원소들을 배열의 한쪽 끝에서만 추가하거나 제거한다.

- 이 한쪽 끝을, 스택의 “top”이라고 부른다.

- 프로그램 스택은 메모리의 특정 영역에 위치한다.

- 스택의 top 원소가, 스택에서 가장 낮은 주소를 갖는다. , “아래 방향으로 성장한다.

- 따라서 관습적으로 스택을 표현할 땐, 스택의 top이 아래쪽으로 위치하도록 그린다.

- 스택 포인터 “%rsp"는 스택 맨 위(top) 원소의 주소를 저장한다.

 

스택 데이터에 대한 인스트럭션

popq, pushq 한 개의 오퍼랜드를 사용한다.

popq

- 데이터를 추출하는 인스트럭션

- 추출을 위한 데이터 목적지를 오퍼랜드로 갖는다.

pushq

- 데이터를 추가하는 인스트럭션

- 추가할 소스 데이터를 오퍼랜드로 갖는다.

 

쿼드워드 값(ex) %rbp)을 스택에 추가하는 과정

- 스택 포인터(%rsp)8만큼 감소시킨다. ( 스택은 주소가 감소할수록 확장되는 것이다 )

- 추가할 쿼드워드 값을 top(%rsp)에 기록한다.

pushq %rbp 명령으로 추가할 수 있다. 이 명령어의 기능은 아래 명령어 두 줄과 동일하다.

subq $8, %rsp -> 스택 포인터를 8만큼 감소시킨다

movq %rbp, (%rsp) -> 스택 포인터 위치에 %rbp 값을 추가한다.

 

그러나 위 2개의 인스트럭션은 8바이트가 필요하지만, pushq 인스트럭션은 1바이트의 기계어 코드로 인코딩된다는 점이 다르다.

 

쿼드워드 값(ex) %rax)을 스택에서 제거하는 과정

- 스택 포인터(%rsp)위치에서 데이터를 읽는다.

- 스택 포인터(%rsp)8만큼 증가시킨다.

popq %rax 명령으로 제거 가능하다. 이 명령어의 기능은 아래 명령어 두 줄과 동일하다.

movq (%rsp), %rax -> 현재 스택 포인터의 데이터를 복사한다

addq $8, %rsp -> 스택 포인터의 값을 8만큼 증가시킨다.

 

스택은 여러 프로그램의 데이터가 다 함께 동일한 메모리에 저장된다. 따라서 표준 메모리 주소지정방법을 이용해 스택 내 특정 데이터에 접근할 수 있게 한다.

스택 top 원소가 쿼드워드일 때, ”movq 8(%rsp), %rdx“ 인스트럭션으로 두번째 데이터에 접근하여 %rdx 레지스터에 복사할 수 있다.

728x90
반응형