정글 20일차

윤종성·2024년 7월 20일
0

알고리즘 공부

목록 보기
13/21

3. 프로그램의 기계수준 표현

x86-64에 기초해 어셈블리어를 배우고, 이것이 컴퓨터를 어떻게 조작하는지를 배운다.
어셈블리어는 ATT표기법을 따른다.

3-1. 역사적 관점

32비트 아키텍처까지 이어오던 인텔의 x86구조를 따라 만들던 AMD가 64비트 전환에서는 치고 나왔다는 그런 이야기.
고대부터 호환성을 64비트까지 끌고 오면서 역사적 관점에서만 이해가 가능한 부분도 있다는 언급도 있다.
인텔의 IA64가 어떻게 망했는지 찾아보는 것도 재미있다.

3-2. 프로그램의 인코딩

C코드는 어셈블리어를 거쳐 실행파일로 만들어진다.

3-2-1. 기계수준 코드

컴퓨터 시스템은 세부구현을 감추기 위해 여러가지 추상화를 사용하고 있다.
1. 기계수준 프로그램의 형식과 동작은 인스트럭션 집합구조(ISA. 흔히 명령어 셋, CPU 아키텍처라고 부르는)에 의해 정의된다.
2. 기계수준 프로그램(기계어 코드)이 사용하는 메모리 주소는 가상주소이다. 메모리매우 큰 바이트 배열인 것 처럼 주소를 넓게 사용하고, 운영체제가 실제 메모리에 입력과 출력을 관리한다.
어셈블리 코드는 기계어 코드와 함께 저수준 언어로 분류된다.
기계어 코드는 바이너리라 그대로 읽기에는 의미를 파악하기 힘들기 때문에, 바이너리인 기계어 코드를 문자로 구현한 것이 어셈블리 코드라고 이해할 수 있다.

3-3. 데이터의 형식

  • x86-64에서 자료형의 길이 별 접미사
    Byte(1byte): b
    Word(2bytes): w
    Double word(4bytes): l
    Quad word(8bytes): q
    Single precision(4bytes): s
    Double precision(8bytes): l

3-4. 정보 접근하기

레지스터는 프로그램 카운터, 정수 레지스터, 조건코드 레지스터, 벡터 레지스터가 있다.
정수 레지스터의 종류:

3-4-1. 오퍼랜드 식별자

어셈블리어 명령어 대부분은 오퍼랜드(피연산자)가 필요하다.
오퍼랜드는 연산을 수행할 소스 오퍼랜드 와 결과를 저장할 목적지 오퍼랜드 가 있다.
소스 오퍼랜드로는 1. 상수, 2. 레지스터, 3. 메모리를 지정할 수 있다.
목적지 오퍼랜드로는 1. 레지스터, 2. 메모리를 지정할 수 있다.

오퍼랜드의 지정 대상에 따른 종류와 표시법:
1. 즉시값(immediate)
현재 참조 중인 프로그램 데이터 자체를 지정한다.(상수를 지정한다는 뜻)
상수 앞에 $를 붙여 표시한다.

  • 표기법: $ImmImm
  • 오퍼랜드 값: ImmImm(상수 자체가 오퍼랜드 값이 된다.)
  • 예시: $-577, $0x1F
  1. 레지스터(register)
    레지스터의 값을 지정한다.
    • 표기법: rar_a
    • 오퍼랜드 값: R[ra]R[r_a] (R[ra]R[r_a]은 레지스터 rar_a에 담겨있는 값에 접근)
    • 예시: %rax, %eax
  2. 유효주소(effective address)
    메모리 주소를 전달하여 그 주소에 접근하는 방식.
    주소를 계산하도록 할 수도 있다.
    • 표기법: ImmImm, Imm(rb,ri,s)Imm(r_b,r_i, s)
    • 오퍼랜드 값: M[Imm]M[Imm], M[Imm+R[rb]+R[ri]×s]M[Imm + R[r_b] + R[r_i]×s] (M[Imm]M[Imm]은 메모리 주소 ImmImm에 담겨있는 값에 접근)
    • 예시: 0x104, (%rax, $rdx, 4)

3-4-2. 데이터 이동 인스트럭션

3-4-2-1. MOV클래스

데이터를 복사하는 명령어인 MOV클래스에 대해 배운다.
MOV클래스는 접미사에 따라 네 개로 구성된다.
네 인스트럭션은 다른 크기의 데이터에 대해 계산한다는 점만 다르다.

MOV클래스의 종류:
1. movb
2. movw
3. movl
4. movq

  • 사용 예:
    movb $0xAF, %al

오퍼랜드로 레지스터를 지정하는 경우, 레지스터의 크기는 접미사가 의미하는 크기와 동일해야 한다.
x86-64에서는 메모리에서 메모리로 값을 복사할 수 없다.(두 오퍼랜드를 모두 메모리로 지정할 수 없다.)

명령어 별 예외

  • movl명령어는 목적지 오퍼랜드로 레지스터를 지정하는 경우 지정된 레지스터 상위 4바이트의 값을 0으로 변경한다.
    movl명령어를 제외한 나머지는 지정된 레지스터의 값만 변경한다.
    예시:
    %al 레지스터에 값을 쓰도록 movb명령어를 사용한 경우 %al레지스터 공간을 제외한 (동일한 레지스터 공간을 공유하는) %ax레지스터의 값은 유지된다.

    이렇게 설계한 이유는 32비트 호환성을 위해서라고 추측된다.
    0확장을 하지 않는 경우 movl로 레지스터에 기록한 데이터는 앞 32비트만 의미가 있고 사용할 수 있다.
    그로 인한 호환성 충돌의 경우는 지금은 쉽게 떠오르지 않는다.
    마찬가지로 8비트, 16비트에서 확장된 x86의 역사를 생각하면 movw과 movb에는 적용되지 않는 것이 의문이지만, 어차피 소급하여 변경할 수 없었을 것이다.
    x86-64는 이전과 다르게 AMD가 개발하였다는 점도 원인일 수 있을 것 같다.

  • movq명령어는 소스 오퍼랜드로 상수를, 목적지 오퍼랜드로 레지스터를 지정하는 경우 32비트인 2의 보수 숫자로 나타낼 수 있는 상수만을 소스 오퍼랜드로 갖는다.
  • movabsq는 소스로 64비트 상수 목적지로 레지스터를 갖는 경우에 사용한다.

3-4-2-2. MOVZ, MOVS 클래스

MOV 클래스와 유사하나 소스를 확장하여 이동한다.
MOVZ 클래스의 명령어들은 목적지의 남은 바이트를 모두 0으로 채워 확장한다.
MOVS 클래스의 명령어들은 부호확장을 통해 목적지의 남은 바이트를 채워 확장한다.
뒤에 소스와 목적지의 바이트 길이 접미사를 붙여 사용한다. 목적지가 소스보다 길이가 길어야 한다.
unsigned 자료형을 확장할 때 MOVZ 클래스를 사용한다.
signed 자료형을 확장할 때 MOVS 클래스를 사용한다.

MOVZ 클래스의 종류(movzlq는 movl과 동일한 기능을 수행하므로 존재하지 않는다.)

  1. movzbw
  2. movzbl
  3. movzwl
  4. movzbq
  5. movzwq

MOVS 클래스의 종류

  1. movsbw
  2. movsbl
  3. movswl
  4. movsbq
  5. movswq
  6. movslq

3-4-3. 데이터 이동 예제

// C코드
void casting(char *sp, int *dp) {
	*dp = (int) *sp
}

void casting2(int *sp, unsigned char *dp) {
	*dp = (unsigned char) *sp
}

이 코드를 어셈블리로 표현하면 다음과 같다.
(sp의 값은 %rdi에 dp의 값은 %rsi에 저장되어 있다)

casting:
  movsbl (%rdi), %eax
  movl %eax, (%rsi)
casting2:
  movl (%rdi), %eax
  movb %al, (%rsi)

3-4-4. 스택 데이터의 저장과 추출(push, pop)

pushq 명령어와 popq 명령어를 사용한다.
pushq %rbp 명령어는 먼저 스택 포인터를 감소시키고 메모리에 %rbp값을 저장한다.

subq $8, %rsp
movq %rbp, (%rsp)

위 코드와 완전히 동일한 작업을 수행하나 pushq %rbp 명령어가 더 인코딩 효율적이다.
popq %rax 명령어는 반대로 스택에서 값을 읽은 후 스택 포인터를 증가시킨다.

movq (%rsp), %rax
addq $8, %rsp

와 동일하다.

3-7. 프로시저

프로시저는 다른 언어에서의 함수와 유사한 개념으로, 특정 작업을 수행하기 위해 작성된 코드 블록이다.
각 프로시저는 고유한 이름을 가지며, 프로그램의 다른 부분에서 호출할 수 있다.
프로그램에서 주요한 추상화이며, 함수처럼 구체적인 구현을 감춰주는 추상화 메커니즘으로도 이용한다.

3-7-1. 런타임 스택

프로시저는 데이터를 스택에 저장한다.
다른 프로시저를 호출하기 위해서는 필요한 스택 프레임의 공간만큼 스택을 늘려야 한다.
saved registers, local variables, argument build area에 무엇이 저장되는지는 뒤에서 언급

3-7-2. 제어의 이동

프로시저를 호출하면 프로그램 제어를 호출된 프로시저로 전달한다.
제어를 프로시저P에서 프로시저Q로 전달하는 것은, 단순히 프로그램 카운터를 프로시저 Q의 시작 코드 주소로 설정하는 것이다.
나중에 프로시저가 리턴(종료)해야할 때에 프로시저 P의 실행을 재개해야 하므로, 프로시저 P는 프로시저 Q를 호출하기 직전에 리턴 후 실행할 명령어(바로 다음 명령어)를 스택에 푸시한다. 이것이 3-7-1. 그림의 Return address이다.
프로시저 Q는 ret 명령어를 통해서 리턴할 때 Return address를 스택에서 팝해와 PC를 설정한다.

3-7-3. 데이터 전송

프로시저가 호출될 때와 리턴할 때, 프로시저는 제어를 전달할 뿐 아니라 데이터를 인자(아규먼트)와 리턴값으로 전달한다.
보통 이런 인자 전달은 레지스터를 통해 일어난다.
x86-64에서 인자 전달을 위해 최대 6개의 인자들을 레지스터를 통해 전달할 수 있다.
전달할 인자가 6개를 넘는 경우 스택을 통해 전달된다.
인자가 6개를 넘으면 프로시저를 호출하기 전 그 넘는 양만큼 스택 프레임에 할당하고 (현재 프로시저의)스택 탑에 넣는다.(3-7-1. 그림의 호출하는 프로시저 P의 스택프레임-Argument n부터 Argument 7부분)
인자들이 모두 배치되고 나면 프로시저를 호출할 수 있다.
만약 호출된 프로시저가 또 다시 인자가 6개를 넘는 다른 프로시저를 호출할 경우에는, 자신의 스택 프레임에 "Argument build area"라고 이름 붙인 영역(3-7-1. 그림 Q 스택 프레임)으로 공간을 할당할 수 있다.

3-7-4. 스택에서의 지역저장공간

프로시저가 지역 데이터를 메모리에 저장해야 하는 경우가 있다:
1. 지역 데이터를 모두 저장하기에는 레지스터의 수가 부족한 경우
2. (C언어에서)지역변수에 연산자 '&'가 사용되었으며, 이 변수의 주소를 생성할 수 있어야 하는 경우
3. 일부 지역변수들이 배열 또는 구조체여서 이들이 배열이나 구조체 참조로 접근되어야 하는 경우
특히 2, 3번의 경우에는 데이터가 적더라도 반드시 메모리 공간을 사용해야 한다.
일반적으로 프로시저는 위와 같은 경우 스택 포인터를 감소시켜서 스택 프레임의 "Local variables"라고 이름 붙인 영역(3-7-1. 그림 Q 스택 프레임)에 공간을 할당한다.

3-7-5. 레지스터를 이용하는 지역저장소

스택과 달리 레지스터들은 모든 프로시저들이 공유한다.
하나의 프로시저(호출자)가 다른 프로시저(피호출자)를 호출할 때, 피호출자가 호출자가 나중에 사용할 레지스터 값을 덮어쓰지 않기 위해서 프로시저들이 준수해야 할 레지스터 사용관습들을 소개하는 장이다.

3-7-5-1. 피호출자-저장 레지스터

%rbx, %rbp, %r12-%r15(3-4. 그림에서 Callee saved로 표시된 레지스터)는 피호출자-저장 레지스터로 구분한다.
피호출자 프로시저는 리턴될 때 호출된 시점과 동일한 레지스터 값들을 보장해야 한다.
그럴 수 있도록 피호출자 프로시저는 이 값들을 변경하지 않거나, 스택에 푸시해 두었다가 리턴하기 전에 팝해오는 방식으로 레지스터를 보존해야 한다.
레지스터 값들을 푸시하면 "Saved registers"라고 이름 붙인 영역(3-7-1. 그림 Q 스택 프레임)을 생성한다.
따라서 저장된 레지스터값-지역 변수들-아규먼트의 스택 프레임 순서가 자연스러워 보인다.

3-7-5-2. 호출자-저장 레지스터

피호출자-저장 레지스터와 스택 포인터(%rsp)를 제외한 모든 레지스터들은 호출자-저장 레지스터로 구분한다.
호출자-저장 레지스터는 피호출자 프로시저로부터 변경될 수 있다는 것을 의미한다.
따라서 호출자 프로시저는 피호출자가 변경해도 되는(의도적으로 변경하도록 하는)레지스터들을 제외하고는, 호출하기 전 이 레지스터들을 저장해야 할 의무가 있다.

3-7-6. 재귀 프로시저

함수와 마찬가지로 재귀적인 방식으로 프로시저를 정의할 수도 있다.
이런 재귀 방식은 별도의 호출들이 서로 간섭하지 않도록 구성된다.
프로시저의 스택운영방식은, 이런 정책을 자연스럽게 제공한다.

profile
알을 깬 개발자

0개의 댓글