Young한 Y86-64로 구조 알아보기 [CSAPP Chap 4]

4장에 돌입함으로써, 현대 CPU의 핵심인 파이프라이닝이 왜 필요한지와 어떻게 성능을 극대화 하는지를 알아본다. 5장도 동시에 하고 있는데, 진짜 이렇게까지 해야 1KB씩 줄이는구나라는 마음에 보다 램 용량 걱정을 덜 하는 시대에 태어난 것은 다행이라고 생각한다.


x86-64를 단순화 한 Y86-64 명령어 집합(ISA)를 살펴보자. 수십 년에 걸쳐 기능이 추가된 x86-64와 달리 교육용 모델 엔진을 사용하는 개념이다.

  • 데이터 이동 (Move):
    • irmovq: immediate → register (상수 값을 레지스터로)
    • rrmovq: register → register (레지스터 간 이동)
    • mrmovq: memory → register (메모리에서 레지스터로)
    • rmmovq: register → memory (레지스터에서 메모리로)
  • 산술/논리 연산 (Operate):
    • OPq: addq, subq, andq, xorq 네 가지 연산을 대표하는 명령어이다.
  • 제어 흐름 (Jump & Call):
    • jmp, jle, jl, je, jne, jge, jg: 조건부/무조건 점프
    • call, ret: 함수 호출 및 복귀
  • 기타:
    • halt: 프로세서 작동을 멈추는 명령어

 

명령어가 꽤 단순하게 배치되었는데 이렇게 프로세서를 설계하는 것은 사용하는 cost가 줄어든다는 이점이 있다.
그리고 명령어가 기계 fit 하냐, 보다 사람 fit하냐에 따른 아키텍처 차이도 있다.

  • Reduced Instruction Set Computer | RISC
    • Y86-64처럼 명령어를 단순하게 나누면 CPU 내부의 회로가 훨씬 간단해진다. 각 명령어는 딱 한 가지 일만 하면 된다.
  • Complex Instruction Set Computer | CISC
      • 사람이 쓰기엔 복잡도가 증가하는 영역으로, CSIC는 컴파일러가 사용하기 편하도록 하나의 명령어가 메모리 접근, 연산 등 여러 복잡한 일을 한 번에 처리 할 수 있도록 만든다.

Y86-64 프로그램의 예제 코드를 같이 보자.

# -- Start of Program ---

irmovq $100, %rax      # 1. 숫자 100을 %rax 레지스터에 넣는다.
irmovq $200, %rbx      # 2. 숫자 200을 %rbx 레지스터에 넣는다.
addq   %rax, %rbx      # 3. %rax의 값과 %rbx의 값을 더해서 결과를 %rbx에 저장한다.
halt                   # 4. 프로세서 작동을 멈춘다.

일단은 사람이 읽을 수는 있는 형태이다.

이 어셈블리 코드를 컴퓨터가 실행 할 수 있는 바이트로 전환하는 인코딩 과정은 어떻게 이루어질까?

각 어셈블리 명령어에 해당하는 고유한 바이트 조합을 미리 약속해준다.
이걸 명령어 집합 구조 | Instruction Set Architecture, ISA 라고 부르는데,
어셈블러는 이 약속에 따라 텍스트로 된 어셈블리 코드를 기계어 바이트로 번역하는 역할을 한다.

예를 들어, irmovq $100, %rax는 10바이트로 전환된다. = 30 F0 64 00 00 00 00 00 00 00

  • 여기선 30irmovq에 대응하고, F0%rax를 말한다. 64부터 끝의 00이 숫자 100을 표현한다.
  • 레지스터끼리 데이터를 옮기는 rrmovq %rax, %rbx는 2바이트에 끝낼 수 있다. 20 : 03
  • 이것은 x86-64 명령어들은 1-15바이트의 길이를 갖는다라는 근거이다.

 

이제 명령어 처리를 알아보자

SEQ(Sequential)라는 이름의 가장 단순한 프로세서 모델을 알아보자.
한 번에 한 명령어만 처리하는 이 모델은 한 명령어의 모든 단계를 끝내고 나서 다음을 진행한다.

  1. 인출 | Fetch : 프로그램 카운터가 가리키는 메모리 주소로 가서, 실행 할 명령어들의 바이트들을 읽어온다.
  2. 해독 | Decode : 읽어온 명령어들을 해독해서 명령어의 내용이나 필요 레지스터를 파악하고 필요한 값을 읽어온다.
  3. 실행 | Execute : 산술 논리 장치가 실제로 계산을 수행한다.
  4. 메모리 | Memory : 만약 명령어가 메모리에 데이터를 쓰거나 읽어야 한다면, 이 단계에서 처리한다.
  5. 쓰기 저장 | Write Back : 계산 결과를 다시 레지스터에 저장한다.
  6. PC 갱신 | PC Update : 다음 명령어를 가리키도록 프로그램 카운터를 갱신한다.

기존에 배웠던 명령어 addq %rax, %rbx에서 :

실제 덧셈 계산은 3단계인 실행에서, %rbx에 저장되는 단계는 5단계일 것이다.

 

실제 이 단계들은 모두 찰나에서 이루어지겠지만, 사이의 단계가 중단되면 그 자체로 남을지에 대한 고민을 해보았다.
정확히 어떻게 이루어지는지는 또 한번 더 깊게 살펴봐야겠지만,

프로세서는 현재 실행 명령어의 프로세스가 전부 이뤄주거나, 전부 이뤄지지 않은 형태 둘 중 하나는 반드시 보장 할 수 있다고 한다.

전원 박살(물리)를 해도 이 내용은 보장이 되나보다. 이걸 원자성(Atomicity)라고 한다.