Byte : 8bit
Word : 16bit
Double Word : 32bit
Quad Word : 64bit
CPU의 16개 범용 레지스터
- 각 범용 레지스터의 크기는 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)” 로 계산된다.
## 위 그림처럼 오퍼랜드는 다양한 형태가 있으나, 마지막 형태를 제외한 다른 형태 들은 일부가 생략된 특별한 경우이다.
## 배열/구조체를 참조하려면 위 그림보다 복잡한 주소지정방식이 필요하다.
아래는 연습문제이다.
왼쪽은 메모리, 오른쪽은 레지스터이다.
메모리와 레지스터를 보고 어떤 값이 담겨있을지 위 문제를 풀어보자.
%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 : byte를 word로 복사
movzbl : byte를 double word로 복사
movzwl : word를 double word로 복사
movzbq : byte를 quad word로 복사
movzwq : word를 quad word로 복사
4바이트 소스를 8바이트 목적지로 확장하는 인스트럭션은 없다. 하지만 이것은 movl로 구현할 수 있다. movl은 mov클래스 중 예외적으로 남은 상위4바이트를 0으로 채우기 때문이다.
MOVS 클래스 : 소스 오퍼랜드의 가장 중요한 비트를 반복해서 복사하는 “부호 확장”으로 채운다.
movsbw
movsbl
movswl
movsbq
movswq
movslq
cltq : 오퍼랜드가 없다. %eax를 소스로, %rax를 목적지로 사용하여 부호 확장 결과를 만든다. movslq %eax, %rax와 정확히 동일한 효과이지만 인코딩이 좀 더 압축적이다.
위 레지스터들의 크기를 참고하여 아래 문제를 풀어보자.
오퍼랜드를 보고 적절한 인스트럭션을 선택하는 문제이다. 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이다.
아래 문제도 풀어보자.
이 명령어들은 에러를 출력한다. 어느 부분이 잘못되었는지 찾으면 된다.
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언어에서의 “포인터”는 어셈블리어에서 단순히 메모리 주소를 의미한다.
-> (%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 레지스터에 복사할 수 있다.
'크래프톤 정글 > TIL' 카테고리의 다른 글
[크래프톤 정글 5기] 스택, 레지스터, 꼬리 재귀 최적화 (0) | 2024.04.09 |
---|---|
[크래프톤 정글 5기] 플로이드 워셜 알고리즘 + 파이썬 구현 (0) | 2024.04.08 |
[크래프톤 정글 5기] week03 알고리즘 주차 열다섯번째 날, DP, 그리디, LCS (0) | 2024.04.04 |
[크래프톤 정글 5기] week02 알고리즘 주차 열네번째 날, 최소 스패닝 트리, 프림 알고리즘, 이분 그래프 (0) | 2024.04.03 |
[크래프톤 정글 5기] week02 알고리즘 주차 열세번째 날, 캐시 메모리, 지역성, 프로세스, 쓰레드 (0) | 2024.04.02 |