Ch3.4 Accessing Information

Park Choong Ho·2021년 4월 28일
0

3.4 Accessing Information

x86-64 CPU는 64-bit 값을 저장하는 general-purpose register 16개를 포함하고 있습니다. 이 레지스터들은 포인터와 integer data를 저장합니다.

Figure3 2

레지스터 모든 이름은 %r로 시작합니다. 그렇지 않은 경우는 인스트럭션 set이 진화한 역사에 기인하는데, 여러가지 서로 다른 naming convention을 따르고 있습니다. 8086은 원래 8개 16-bit 레지스터들을 가지고 있었습니다. %ax부터 %bp가 이 레지스터들입니다. 각각은 특정한 목적에 따라 이름이 지어졌습니다. IA32로 확장되면서, 이 레지스터들은 32-bit 레지스터들로 확장되었고, %eax부터 %ebp가 되었습니다. x86-64로 확장할때는, 원래 있었던 레지스터 8개가 64-bit로 커지고 %rax 부터 %rbp로 불리게 되었습니다. 이에 더해, 8개의 새로운 레지스터가 추가되었습니다. 그리고 그들은 naming convention에 따라 %r8부터 %r15가 되었습니다.

위 그림에 안쪽으로 겹쳐진 박스만큼, 각 인스트럭션들은 16개 레지스터 안에서 low-order 바이트 크기 만큼 데이터를 다룰 수 있습니다. 이에 따라 8-bit는 1 바이트, 16-bit 계산은 2 바이트, 32-bit 계산은 4 바이트, 64-bit 계산은 전체 레지스터만큼 데이터를 다룹니다.

이후 섹션들에서 1, 2, 4, 8 바이트 값을 복사하고 생성하는 여러가지 인스트럭션을 확인하겠습니다. 인스트럭션들이 레지스터를 목적지로 가지고 있을때, 만약 8 바이트보다 더 적은 값을 생성하는 경우 해당 레지스터에 남아있는 바이트들이 어떻게 되는지 두 가지 convention을 살펴보겠습니다. 1, 2바이트를 생성하는 경우, 남아있는 바이트들을 변화시키지 않습니다. 4바이트는 남은 4 바이트를 0으로 설정합니다. 후자는 IA32에서 x86-64로 넘어오면서 생긴 convention입니다.

그림 맨 오른쪽의 annotation에서 확인할 수 있듯이, 레지스터들은 프로그램에서 각기 다른 역할을 수행합니다. 이들 중 가장 유니크한 것은 stack pointer(%rsp)인데, 런타임 스택에서 마지막 위치를 나타내는데 사용됩니다. 몇몇 인스트럭션들은 이 레지스터를 읽고 쓰는데 특화되어 있습니다. 다른 15개의 레지스터들은 사용에 있어 다소 유연합니다. 적은 숫자의 인스트럭션들만이 이 레지스터들을 사용할 수 있습니다. 더 중요한 것은, 기준 프로그래밍 convention들이 레지스터가 어떻게 스택을 관리하고, 함수 인자를 넘기고, 함수로 부터 값을 반환하고, 지역 및 임시 변수들을 저장하는지 결정합니다. 이 convention들은 3.7장에서 다룹니다.

3.4.1 Operand Specifiers

대부분 인스트럭션들은 하나 또는 더 많은 operand를 가지고 있습니다. operand는 실행에 활용되는 source value와 결과를 저장하는 destination location로 구분됩니다. x86-64는 여러가지 형태의 operand를 지원합니다.

Figure3.3

Source Value는 레지스터 또는 메모리에서 읽어오거나 상수로 주어집니다. 결과는 메모리나 레지스터에 저장됩니다. operand 경우의 수는 총 세가지입니다.

첫번째, immediate 는 상수입니다. ATT 어셈블리 코드 형태에서, $ 다음에 interger가 오는 형식입니다 (ex. $-577 $0x1F). 다른 인스트럭션의 경우 immediate값이 다른 범주를 가질 수 있습니다. 어셈블러가 자동으로 해당 값을 인코딩합니다.

두번째, register는 레지스터에 들어있는 값을 나타냅니다. 이 값은 16개(레지스터 갯수)의 1, 2, 4, 8바이트 값을 가집니다. 위 그림을 보면, 우리는 ra가 register a를 가리키게끔 사용하고 있고 이 값은 R[ra] 레퍼런스로 참조합니다.

세번째 operand는 memory 레퍼런스입니다. 우리는 effective address라 불리는 계산된 주소에 따라 특정 메모리에 접근할 수 있습니다. 메모리를 큰 바이트 배열로 보기 때문에, Mb[addr]이라는 notation을 통해 주소 addr부터 시작해서 b byte만큼 값을 참조한다고 나타냅니다. 이를 단순화 하기 위해, 대개 subcript b를 제거합니다.

위 표에서 보는 바와 같이, 메모리 참조를 가능하게 하는 여러 형태의 addressing model이 있습니다. 가장 일반적인 형태는 표 가장 밑에 보이는 Imm(rb, ri, s) 형태입니다. 이런 참조는 4가지로 구성되어 있습니다. Immediate offset인 Imm, base 레지스터인 rb, index 레지스터인 ri, 그리고 scale factor인 s(s는 반드시 1, 2, 4, 8중 하나입니다.). base, index 모두 만드시 64-bit 레지스터여야합니다. 가장 효과적인 주소 계산법은 Imm + R[rb] + R[ri] * s입니다. 이 전형적인 형태는 배열 요소를 참조할 때도 확인 할 수 있습니다. 다른 형태는 이 요소들이 생략된 형태입니다. 나중에 확인하겠지만, 가장 복잡한 addressing model은 구조체 요소와 배열을 참조하는데 유용합니다.

Practice Problem 3.1

Practice Problem3.1

3.4.2 Data Movement Instructions

가장 많이 사용되는 인스트럭션은 데이터를 한 곳에서 다른 곳으로 복사하는 인스트럭션입니다. 피연산자 범용성을 통해 데이터를 옮기는 인스트럭션으로 여러 인스트럭션을 표현할 수 있습니다. source, destination 타입을 달리하는 것부터, 어떤 전환을 하느냐 그리고 어떤 부작용을 가지고 있느냐에 따라 여러 인스트럭션을 표현할 수 있습니다.

인스트럭션들을 instruction class로 묶도록 하겠습니다. 인스트럭션 클래스란, 같은 동작을 하지만 서로 다른 피연산자 크기를 가지는 인스트럭션 집합입니다.

아래 표는 가장 단순한 데이터 복사를 나타냅니다. (MOV class) 이 인스트럭션들은 source에서 destination으로 데이터를 변화없이 복사합니다. 해당 클래스는 movb, movw, movl, movq 4가지 인스트럭션으로 구성됩니다. 해당 인스트럭션들은 유사하게 동작하는데, 각각 1, 2, 4, 8 바이트 크기 데이터에 대응합니다.

Figure 3.4

source 피연산자는 상수값, 레지스터, 또는 메모리에 저장되어 있는 값을 가리킵니다. destination 피연산자는 메모리 또는 레지스터 주소를 가리킵니다. x86-64는 move 인스트럭션이 가지고 있는 source, destination 피연산자 모두 메모리 위치가 되서는 안되게 강제하고 있습니다.(둘다 되면 안되는 거지, 둘 중 하나는 가능합니다.) 값을 한 메모리에서 다른 메모리로 복사하는데 두가지 인스트럭션이 필요합니다.

첫번째는 source value를 레지스터에 로드하고 두번째는 이 레지스터 값을 destination에 쓰는 것입니다. 레지스터 피연산자는 16개의 레지스트중 아무거나 가능합니다. 다만 레지스터 사이즈는 인스트럭션의 마지막 문자(b,w,l or q)와 반드시 일치해야 합니다. 대부분의 경우 MOV 인스트럭션은 오직 destination 피연산자에 의해 나타난 특정 레지스터 바이트나 메모리 위치에 있는 값만 업데이트합니다. 예외는 movl 명령어가 레지스터를 destination으로 가질 때인데, 레지스터 high-order 4바이트를 0으로 만듭니다. 이 예외는 x86-64에서 온 특정 규칙으로부터 유래했습니다. 어떤 인스트럭션이든 32-bit value를 레지스트에 생성하면 해당 레지스터 higher order를 0으로 설정합니다.

아래 예시들은 가능한 source, destination 타입 조합 5가지입니다. source 피연산자가 먼저오고 destination 피연산자가 다음에 오는 걸 기억합시다.

movl $0x4050,%eax Immediate--Register, 4bytes
movw %bp,%sp      Register--Register,  2bytes
movb (%rdi,%rcx),%al Memory--Register, 1byte
movb $-17,(%esp)     Immediate--Memory, 1byte
movq %rax,-12(%rbp)  Register--Memory, 8bytes

위 그림에서 마지막 인스트럭션(movabsq)은 64-bit immediate 데이터를 다루기 위한 인스트럭션입니다. 일반적인 movq 인스트럭션은 오직 32-bit 2의 보수로 나타내는 immediate source 피연산자만 가질 수 있습니다. 이 값은 destination에 sign 64-bit까지 나타내는데 사용될 수 있습니다. movabsq인스트럭션은 변화하는 64-bit immediate 값을 source 피연산자로 가질 수 있고 destination으로 레지스터만을 가질 수 있습니다. (이 단락은 이해가 잘 되지 않습니다. 아시는 분은 댓글 남겨주시면 감사하겠습니다.)

Figure 3.5

Figure 3.6

위 그림들은 더 작은 source value에서 더 큰 destination으로 데이터를 복사하는 두 인스트럭션 클래스를 보여주고 있습니다. 이 인스트럭션들은 메모리 또는 레지스터에 저장되어 있는 데이터를 복사해서 레지스터 destination에 복사합니다. MOVZ 클래스 인스트럭션들은 남아있는 데스티네이션 바이트들을 0으로 채웁니다. MOVS 클래스 인스트럭션들은 source 피연산자의 가장 중요한 비트들의 복사본을 만드는 형태로 sign extension을 수행합니다. 각 인스트럭션 이름은 크기 designator 마지막 두 문자로 결정됩니다. 첫 문자는 source 크기를, 그 다음 문자는 destination 크기를 나타냅니다. 각 클래스마다 1, 2 바이트 크기 source와 2, 4 바이트 크기 destination을 다루는 인스트럭션 3개가 존재합니다. 물론 이 경우 모두 destination 크기가 source보다 큽니다.

첫번째 그림에서 4 바이트 source에서 8 바이트 destination으로 zero-extend하는 인스트럭션이 없는 것을 확인할 수 있습니다. 해당 인스트럭션은 movzlq라 명명했겠지만 존재하지 않습니다. 대신, 이 타입의 데이터 복사는 movl 인스트럭션을 사용하여 실행 가능합니다. movl 인스트럭션이 destination 레지스터에 상위 4바이트를 0으로 채우는 특성을 활용한 것입니다. 만약 그렇지 않았다면, 64-bit destination에 sign extension은 모든 3개 타입에 지원되고 zero extension은 2개 source 타입에만 지원되었을 겁니다.

두번째 그림에 cltq 인스트럭션이 있습니다. 이 인스트럭션은 어떤 피연산자도 가지지 않습니다. 언제나 %eax 레지스터를 source로 %rax를 destination으로 가지면서 sign-extension을 실행합니다. 따라서 movslq %eax %rax와 결과가 같지만, 더 작은 인코딩 결과를 가집니다. (인코딩 결과가 더 짧다는 의미인 듯?)

Practice Problem 3.2

Practice Problem 3.2

Practice Problem 3.3

Practice Problem 3.3

  • movb $0xF, (%ebx)
    %ebx 레지스터로는 주소를 참조할 수 없습니다.
  • movl %rax, (%rsp)
    %rax 값을 복사하면 movl이 아닌 movq가 되어야합니다.
  • movw (%rax), 4(%rsp)
    두 피연산자 모두 주소 값이 될 수 없습니다.
  • movb %al, %sl
    %sl이라는 레지스터는 존재하지 않습니다.
  • movq %rax, $0x123
    immediate가 destination에 있을 수 없습니다.
  • movl %eax, %rdx
    %rdx로 복사하는 것이므로 movq가 되어야합니다.
  • movb %si, 8(%rbp)
    %si 값을 복사하면 movw가 되어야합니다.

3.4.3 Data Movement Example

long exchange(long *xp, long y){
	long x = *xp;
	*xp = y;
	return x;
}
long exchange(long *xp, long y)
xp in %rdi, y in %rsi

1 exchange:
2	movq (%rdi), %rax Get x at xp. Set as return value
3	movq %rsi, (%rdi) Store y at xp.
4	ret

위 코드를 보면 exchange 함수는 3가지 인스트럭션을 실행합니다. movq 데이터 복사 2번 그리고 ret 1번입니다. ret는 함수를 호출했던 위치로 되돌아가는 인스트럭션입니다. 자세한 얘기는 3.7에서 살펴보겠습니다. 그때까지는, 레지스터에 있는 함수에 인자들이 넘어갔다고 봐도 충분합니다. 어셈블리 코드에 추가적으로 기입한 내용들이 이 내용입니다. 함수는 %rax 레지스터 전체 또는 낮은 순위를 차지하는 부분부터 값을 저장해서 값을 반환합니다.

해당 절차가 실행되면, xp, y 인자들은 각각 %rdi와 %rsi에 저장됩니다. 인스트럭션 2는 메모리로부터 x를 읽어오고 값을 %rax에 저장합니다. 이것이 x = *xp C 코드 어셈블리 구현입니다. 그 다음, %rax를 해당 함수 반환 값으로 활용합니다. 인스트럭션 3은 %rdi에 저장된 xp 메모리 위치에 y 값을 씁니다. *xp = y의 실행입니다. 이 예시에서 MOV인스트럭션들이 어떻게 레지스터로부터 메모리를 읽어오고 어떻게 레지스터 값을 메모리에 쓰는지 확인할 수 있습니다.

이 어셈블리 코드 2가지 특징을 기억해 둡시다.

첫째, C에서 포인터는 주소입니다. 포인터를 참조하는 것은 포인터를 레지스터에 복사하는 것과 이 레지스터를 메모리 참조에 사용하는 것 모두 포함합니다.

둘째, x 같은 지역 변수들은 종종 메모리보다 레지스터에 저장됩니다. 레지스터 접근은 일반 메모리 접근에 비해 훨씬 빠릅니다.

Practice Problem 3.4

Practice Problem 3.4

Practice Problem 3.4

src_tdest_tInstruction
longlongmovq (%rdi), %rax
movq %rax, (%rsi)
charintmovsbl (%rdi), %eax
movl %eax, (%rsi)
charunsignedmovsbl (%rdi), %eax
movl %eax, (%rsi)
unsigned charlongmovsbq (%rdi), %rax
movq %rax, (%rsi)
intcharmovl (%rdi), %al
movb %al, (%rsi)

Practice Problem 3.5

Practice Problem 3.5

3.4.4 Pushing and Popping Stack Data

마지막 두번의 데이터를 움직이는 연산은 데이터를 스택에 올리거나 스택으로 부터 빼내기 위해 사용됩니다.

Figure3.8

스택은 프로시처 호출을 다루는데 있어 핵심적인 역할을 합니다. 스택은 값들이 "last in, first out" 일명 LIFO를 통해서 더해지고 삭제되는 데이터 구조입니다. push 연산을 통해 데이터를 스택에 올리고 pop 연산을 통해 데이터를 제거합니다. 여기서 pop되는 값은 항상 가장 최근에 스택에 올라간 값입니다. 스택은 배열처럼 구현될 수 있는데, 항상 배열 끝 부분에 요소를 더하거나 제거하는 형식으로 할 수 있습니다. 이 배열 끝 부분을 배열의 top이라고 합니다. x86-64에서는 프로그램 스택이 메모리 특정 부분에 저장되어 있습니다.

Figure3.9

위 그림처럼, 스택은 top이 가장 낮은 주소를 가지고 있으면서 아래로 요소들을 쌓아나갑니다. (스택 자료구조를 부석할 때는 대개 실제 메모리 구조와는 다르게 거꾸로 그립니다. 이때는 탑이 bottom쪽에 있게 됩니다.) 스택 포인터 %rsp는 top 스택 요소의 주소를 가지고 있습니다.

pushq 인스트럭션은 스택에 데이터를 넣는 연산을 제공합니다. popq 인스트럭션은 반면에 스택에서 데이터를 빼내는 연산을 제공합니다. 각 인스트럭션은 하나의 피연산자만을 가집니다. pushing에는 source data, poping에는 data destination입니다.

quad word를 스택에 넣는 것은 먼저 스택 포인터를 8만큼 낮추는 것을 실행하고 그리고 값을 새로운 스택 top 주소가 참조하는 메모리에 써줍니다. 따라서, pushq %rbp 인스트런션은 아래 인스트럭션과 같은 결과를 가져옵니다.

subq $8, %rsp     Decrement stack pointer
movq %rbp, (%rsp) Store %rbp on stack

다만 pushq 인스트럭션은 machine code에서 하나의 바이트로 인코딩되는 반면에, 위 어셈블리의 경우에는 총 8 바이트가 필요합니다. 위 그림에서 보는 바와 같이, %rsp가 0x108이고 %rax가 0x123일 때 pushq %rax를 실행하는 것이 어떤 결과를 낳는지 확인할 수 있습니다. 먼저 %rsp가 8만큼 줄어들며 0x100 값을 가지게 되고 0x100 주소의 메모리에 0x123이 저장됩니다.

quad word를 빼내는 것은 스택 top에서 데이터를 읽어오고 stack pointer를 8만큼 증가시키는 것까지 포함합니다. 따라서 popq %rax 인스트럭션은 아래 어셈블리와 동일합니다.

movq (%rsp), %rax Store %rax from stack
subq $8, %rsp     Increment stack pointer

위 그림 3번째표를 보면 popq %rdx인스트럭션를 실행함으로써의 결과를 확인할 수 있습니다. 값 0x123을 메모리로부터 읽어오고 이를 %rdx 레지스터에 씁니다. 그 다음 %rsp 레지스터가 0x100에서 0x108만큼 증가합니다. 그림에서처럼 0x123은 메모리 위치 0x100에 남아 있습니다. 다른 pushq 인스트럭션에 의해 덮어쓰이게 될때까지 남습니다. 하지만, 스택 top은 항상 %rsp가 가리키는 값입니다.

스택은 프로그램 코드와 다른 형태의 프로그램 데이터와 같은 메모리에 포함되므로, 프로그램은 stack안에 자유롭게 변경되는 위치를 표준 메모리 주소 methods를 통해 접근할 수 있습니다. 예를 들어, 스택의 top 요소가 quad word라고 가정해 보면 인스트럭션 movq 8(%rsp), %rdx는 스택의 두번째 quad word를 %rdx에 복사하게 됩니다.

profile
백엔드 개발자 디디라고합니다.

0개의 댓글