Krafton Jungle/4. CSAPP

[Computer System] ③ 프로그램의 기계수준 표현 (1)

munsik22 2025. 4. 4. 19:34

3.1 역사적 관점

무어의 법칙(Moore's law)은 반도체 집적회로의 성능이 24개월마다 2배로 증가한다는 법칙이다. 경험적인 관찰에 바탕을 두고 있다. 인텔의 공동 설립자인 고든 무어가 1965년에 내 놓은 것이다.

"The complexity for minimum component costs has increased at a rate of roughly a factor of two per year... Certainly over the short term this rate can be expected to continue, if not to increase. Over the longer term, the rate of increase is a bit more uncertain, although there is no reason to believe it will not remain nearly constant for at least 10 years. That means by 1975, the number of components per integrated circuit for minimum cost will be 65,000. I believe that such a large circuit can be built on a single wafer."

 

하지만 기술 수준이 발전하면서 무어의 법칙은 더 이상 성립하기 어려워진 상태이다. 반도체 소자 단위를 100nm에서 50nm로 줄이는 것은 비교적 쉬웠지만, 3nm에서 절반을 줄이는 것이 훨씬 더 어렵다.


3.2 프로그램의 인코딩

3.2.1 기계수준 코드

x86-64를 위한 기계어 코드는 본래의 C코드와는 상당히 다르다. 프로세서의 상태는 C 프로그래머에게는 일반적으로 감추어져 있다.

  • 프로그램 카운터 : 실행할 다음 인스트럭션의 메모리 주소를 가리킴. 일반적으로 PC라고 하며, x86-64에서는 %rip라고 한다.
  • 정수 레지스터 파일 : 64비트 값을 저장하기 위한 16개의 이름을 붙인 위치를 가짐. 주소(C언어의 포인터에 해당)나 정수 데이터를 저장할 수 있다. 프로그램의 중요한 상태를 추적하거나, 함수의 리턴 값뿐만 아니라 프로시저의 지역변수와 인자 같은 임시 값을 저장하는 데 사용하기도 한다.
  • 조건코드 레지스터 : 가장 최근에 실행한 산술 또는 논리 인스트럭션에 관한 상태 정보를 저장. if나 while문을 구현할 때 필요한 제어나 조건에 따른 데이터 흐름의 변경을 구현할 때 사용된다.
  • 벡터 레지스터들의 집합 : 하나 이상의 정수나 부동소수점 값들을 각각 저장할 수 있다.

C가 다른 종류의 데이터 타입을 선언하고 메모리에 할당할 수 있는 모델을 제공하는 반면, 기계어 코드는 메모리를 바이트 주소지정이 가능한 큰 배열로 본다.

 

C에서 배열과 구조체 같은 연결된 데이터 타입들은 기계어 코드에서는 연속적인 바이트들로 표시된다. 스칼라(scalar) 데이터 타입의 경우에도 어셈블리 코드는 부호형과 비부호형, 다른 타입의 포인터들, 심지어 포인터와 정수형 사이에도 구분을 하지 않는다.

 

프로그램 메모리는 다음을 포함한다.

  • 프로그램의 실행 기계어 코드
  • 운영체제를 위한 일부 정보
  • 프로시저 호출과 리턴을 관리하는 런타임 스택
  • 사용자에 의해 할당된 (ex. malloc 라이브러리 함수) 메모리 블록

OS는 가상 주소공간을 관리해서 가상주소를 실제 프로세서 메모리 상의 물리적 주소 값으로 번역해 준다.

 

하나의 기계어 인스트럭션은 매우 기초적인 동작만을 수행한다.

  • 레지스터들에 저장된 두 개의 수를 더하거나
  • 메모리와 레지스터 간에 데이터를 교환하거나
  • 새로운 인스트럭션 주소로 조건에 따라 분기하는 등

컴파일러는 일련의 인스트럭션을 생성해서 산술연산식의 계산, 반복문 프로시저 호출과 리턴 등의 프로그램 구문을 구현해야 한다.

3.2.2 코드 예제

long mult2(long, long);
void multstore(long x, long y, long *dest) {
    long t = mult2(x, y);
    *dest = t;
 }

mstore.c

gcc -Og -S mstore.c
multstore:
    pushq	%rbx
    movq	%rdx, %rbx
    call	mult2
    movq	%rax, (%rbx)
    popq	%rbx
    ret

mstore.s

gcc -Og -c mstore.c
53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3

mstore.o

objdump -d mstore.o
0000000000000000 <multstore>:
	0:	53			push		%rbx
	1:	48 89 d3		mov		%rdx,%rbx
	4:	e8 00 00 00 00	callq	9 <multstore+0x9>
	9:	48 89 03		mov		%rax,(%rbx)
	c: 	5b			pop		%rbx
	d:	c3			retq

Disassembly of function multstore in binary file mstore.o

#include <stdio.h>
int main() {
    long d;
    multstore(2, 3, &d);
    printf("2 * 3 --> "%ld\n", d);
    return 0;
}
long mult2(long a, long b) {
	long s = a * b;
    return s;
}

main.c

gcc -Og -o prog main.c mstore.c
objdump -d prog
0000000000400540 <multstore>:
	400540:	53			push		%rbx
	400541:	48 89 d3		mov		%rdx,%rbx
	400544:	e8 00 00 00 00	callq	9 <multstore+0x9>
	400549:	48 89 03		mov		%rax,(%rbx)
	40054c: 5b			pop		%rbx
	40054d:	c3			retq

Disassembly of function multstore in binary file prog

3.2.3 형식에 대한 설명

multstore:
    pushq	%rbx			# Save %rbx
    movq	%rdx, %rbx		# Copy dest to %rbx
    call	mult2			# Call mult2(x, y)
    movq	%rax, (%rbx)		# Store result at *dest
    popq	%rbx			# Restore %rbx
    ret					# Return

mstore.s


3.3 데이터의 형식

인텔 프로세서들이 근본적으로 16비트 구조를 사용하다가 추후에 32비트로 확장했기 때문에 인텔은 워드라는 단어를 16비트 데이터 타입을 말할 때 사용한다. 32비트는 더블워드라고 부르고, 64비트는 쿼드워드라고 부른다.

C declaration Intel data type Assembly-code suffix Size (bytes)
char Byte b 1
short Word w 2
int Double word l 4
long Quad word q 8
char * Quad word q 8
float Single precision s 4
double Double precision l 8

x86-64에서 C자료형의 길이: 64비트 머신에서 포인터는 8바이트의 길이를 가진다.


3.4 정보 접근하기 ✅

 x86-64 주처리장치 cpu는 64비트 값을 저장할 수 있는 16개의 범용 레지스터를 보유하고 있다. 아래 그림은 레지스터이며, 레지스터는 정수데이터와 포인터를 저장하는데 사용한다.

정수 레지스터: 전체 16개 레지스터의 하위 바이트들은 바이트, 워드(16bit), 더블워드(32bit), 쿼드워드(64bit)씩 접근할 수 있다.


인스트럭션들은 16개의 레지스터 하위바이트들에 저장된 다양한 크기의 데이터에 대해 연산할 수 있다. 16비트 연산들은 가장 덜 중요한 2바이트에 접근하고, 32비트 연산은 덜 중요한 4바이트에, 64비트 전체에 접근 할 수 있다.

일반적인 프로그램에서 서로 다른 레지스터들은 각각의 다른 목적으로 사용된다.

  • %rsp스택포인터런타임 스택의 끝 부분을 가리키기 위해 사용된다.
  • %rip프로그램 카운터(PC)라고도 하며, 실행할 다음 인스트럭션의 메모리 주소를 가리킨다.

3.4.1 오퍼랜드 식별자

대부분의 인스트럭션은 하나 이상의 오퍼랜드를 가진다. 오퍼랜드는 연산 수행 소스source값과 그 결과를 저장할 목적지destination의 위치를 명시한다. 오퍼랜드의 종류는 세 가지 타입으로 구분할 수 있다.

  1. immediate : 상수값을 나타낸다.
    • ATT 형식의 어셈블리 코드에서 상수는 $ 기호 다음에 사용한다.
    • 어셈블러는 값을 인코딩하는 가장 컴팩트한 방법을 자동으로 선택하게 된다.
  2. register : 레지스터의 내용을 나타낸다.
    • 16개의 64비트, 32비트, 8비트 레지스터의 하위 일부분인 8,4,2,1 바이트 중 하나의 레지스터를 가리킨다.
  3. 메모리 참조 : 유효주소라고 부르는 계산된 주소에 의해 메모리 위치에 접근하게 된다.
    • 메모리는 거대한 바이트의 배열로 생각할 수 있다 → Mb[Addr]
    • 여러가지 유형의 메모리 참조를 가능하게 하는 많은 주소 지정방식이 존재한다.

오퍼랜드의 형식: 오퍼랜드는 즉시값(상수), 레지스터 값, 메모리에서 가져오는 값으로 표시할 수 있다. 배율 인자 s는 1, 2, 4, 8 중에 하나가 될 수 있다.

 

 Practice 3.1                                                                                                                                  

다음과 같은 값들이 표시된 메모리 주소와 레지스터에 저장되어 있다.

Address Value Register Value
0x100 0xFF %rax 0x100
0x104 0xAB %rcx 0x1
0x108 0x13 %rdx 0x3
0x10C 0x11    

다음 표에 지시된 오퍼랜드의 값을 채우시오. (드래그하면 정답이 보입니다.)

Operand Value Comment
%rax 0x100 Register
0x104 0xAB Absolute address
$0x108 0x108 Immediate
(%rax) 0xFF Address 0x100
4(%rax) 0xAB Address 0x104
9(%rax,%rdx) 0x11 Address 0x10C
260(%rcx,%rdx) 0x13 Address 0x108
0xFC(,%rcx,4) 0xFF Address 0x100
(%rax,%rdx,4) 0x11 Address 0x10C
더보기
  • 4(%rax) : %rax의 주소값 0x100 + 4 → 0x104에 저장된 값 0xAB
  • 9(%rax,%rdx) : 0x100 + 0x3 + 9 → 0x10C 0xC=1310에 저장된 값 0x11
  • 260(%rcx,%rdx) : 0x1 + 0x3 + 0x104 0x104=20610 → 0x108에 저장된 값 0x13
  • 0xFC(,%rcx,4) : (0x1 * 4) + 0xFC → 0x100에 저장된 값 0xFF
  • (%rax,%rdx,4) : 0x100 + (0x3 * 4) → 0x10C에 저장된 값 0x11

3.4.2 데이터 이동 인스트럭션

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

  • movb : Move byte (1 byte)
  • movw : Move word (2 bytes)
  • movl : Move double word (4 bytes)
  • movq : Move quad word (8 bytes)

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

 

소스 오퍼랜드는 상수, 레지스터 저장 값, 메모리 저장 값을 표시한다.목적 오퍼랜드는 레지스터 또는 메모리 주소의 위치를 지정한다. 하나의 메모리 위치에서 다른 위칠 어떤 값을 복사하기 위해서는 두 개의 인스트럭션이 필요하다.

  • 소스 값을 레지스터에 적재하는 인스트럭션
  • 레지스터의 값을 목적지에 쓰기 위한 인스트럭션
movl $0x4050,%eax		# Immediate--Register,	4 bytes
movw %bp,%sp			# Register--Register,	2 bytes
movb (%rdi,%rcx),%al		# Memory--Register,	1 byte
movb $-17,(%rsp)		# Immediate--Memory,	1 byte
movq %rax,-12(%rbp)		# Register--Memory,	8 bytes

 

다음 인스트럭션들은 보다 작은 소스값을 더 큰 목적지로 복사할 때 사용하기 위한 데이터 이동 명령들이다.

Instruction Description
movzbw Move zero-extended byte to word
movzbl Move zero-extended byte to double word
movzwl Move zero-extended word to double word
movzbq Move zero-extended byte to quad word
movzwq Move zero-extended word to quad word

0으로 확장하는 데이터 이동 인스트럭션: 이 인스트럭션들은 레지스터나 메모리를 소스로 가지며, 레지스터를 목적지로 가진다.

[데이터 이동이 목적지 레지스터를 변경하는 방법 이해하기]
movabsq $0x0011223344556677, %rax			# %rax = 0011223344556677
movb	$-1, %al					# %rax = 00112233445566FF
movw	$-1, %ax					# %rax = 001122334455FFFF
movl	$-1, %eax					# %rax = 00000000FFFFFFFF
movq	$-1, %rax					# %rax = FFFFFFFFFFFFFFFF


앞으로 우리는 16진수 표기를 사용한다. 이 예제에서 movabsq 인스트럭션은 레지스터 %rax0011223344556677로 초기화한다. 나머지 인스트럭션들은 즉시값immediate -1을 소스 값으로 가진다. -1의 16진수 표시는 FF…F 형태이, 여기서 F의 개수는 표시하는 바이트 수의 2배이다. 따라서 movb 인스트럭션은 %rax의 하위바이트를 FF로 설정하지만, movw 인스트럭션은 하위 2바이트를 FFFF로 설정하며 다른 바이트들은 그대로 둔다. movl 인스트럭션은 하위 4바이트를 FFFFFFFF로 설정하지만, 이것은 또한 상위 4바이트를 00000000으로 설정한다. 마지막으로 movq 인스트럭션은 레지스터 전체를 FFFFFFFFFFFFFFFF로 설정한다.

 

다음은 cltq 인스트럭션이다. 이 인스트럭션은 오퍼랜드가 없다. 언제나 레지스터 %eax를 소스로, %rax를 목적지로 사용해서 부호 확장 결과를 만든다. 그래서 이것은 movslq %eax, %rax와 동일한 효과를 내지만, 조금 더 압축적인 인코딩을 가진다.

Instruction Description
movsbw Move sign-extended byte to word
movsbl Move sign-extended byte to double word
movswl Move sign-extended word to double word
movsbq Move sign-extended byte to quad word
movswq Move sign-extended word to quad word
movslq Move sign-extended double word to quad word
cltq Sign-extend %eax to %rax

부호를 확장하는 데이터 이동 인스트럭션: MOVS 인스트럭션들은 레지스터나 메모리를 소스 위치로 가지며, 레지스터를 목적지로 가진다. cltq 인스트럭션은 레지스터 %eax%rax만을 대상으로 한다.

 

 Practice 3.2                                                                                                                                  

다음 어셈블리 언어의 각 줄에 대해 오퍼랜드를 고려해 적절한 인스트럭션 접미어를 결정하시오. (드래그하면 정답이 보입니다.)

movl %eax, (%rsp)
movw (%rax), %dx
movb $0xFF, %bl
movb (%rsp,%rdx,4), %dl
movq (%rdx), %rax
movw %dx, (%rax)
더보기

[b = 1바이트 / w = 2바이트 / l = 4바이트 / q = 8바이트]

  1. %eax32비트 레지스터다. 메모리에 32비트(4바이트) 값을 저장하려고 하므로 답은 movl이 된다.
  2. %dx16비트 레지스터다. 메모리에서 16비트 데이터를 읽어와야 하므로 크기는 2바이트 → movw
  3. %bl8비트 레지스터이고, 즉 바이트 단위 연산이다. 상수 0xFF도 1바이트 크기이므로 답은 movb다.
  4. %dl8비트 레지스터다. 메모리에서 1바이트를 읽어오기 때문에 답은 movb가 된다.
  5. %rax64비트 레지스터다. 메모리에서 8바이트 값을 읽어오기 때문에 답은 movq가 된다.

 Practice 3.3                                                                                                                                  

다음 프로그램의 각 라인은 어셈블러를 호출하게 되면 에러 메시지를 생성한다. 각 줄마다 무엇이 잘못인지 설명하시오. (드래그하면 정답이 보입니다.)

movb $0xF, (%ebx) Cannot use %ebx as address register
movl %rax, (%rsp) Mismatch between istruction suffix and register ID
movw (%rax), 4(%rsp) Cannot have both source and destination be memory references
movb %al,%sl No register named %sl
movq %rax,$0x123 Cannot have immediate as destination
movl %eax,%rdx Destination operand incorrect size
movb %si, 8(%rbp) Mismatch between instruction suffix and register ID
더보기

movb $0xF, (%ebx)

💬 답: Cannot use %ebx as address register

  • (%ebx)는 주소로 사용되고 있는데, 64비트 모드에서는 주소 계산에 32비트 레지스터%ebx를 직접 사용할 수 없다.
  • 주소 계산에는 보통 %rbx와 같은 64비트 레지스터를 사용해야 한다.
  • ❌ 문제점: 64비트 주소 모드에서 %ebx는 주소 레지스터로 사용할 수 없음

movl %rax, (%rsp)

💬 답: Mismatch between instruction suffix and register ID

  • %rax는 64비트 레지스터다.
  • 그런데 movl은 32비트(4바이트)를 이동시키는 명령이므로, 오퍼랜드 크기가 일치하지 않는다.
  • movl에 %rax는 사용할 수 없고, %eax를 사용해야 한다.
  • 문제점: movl은 32비트 명령인데, 64비트 레지스터 %rax 사용 → 크기 불일치

movw (%rax), 4(%rsp)

💬 답: Cannot have both source and destination be memory references

  • (%rax)4(%rsp)는 둘 다 메모리 참조다.
  • x86에서는 메모리 ↔ 메모리 직접 복사는 지원하지 않으며, 반드시 레지스터를 중간에 사용해야 한다.
  • 문제점: 소스와 목적지가 모두 메모리 주소 → 허용되지 않는 형식

movb %al, %sl

💬 답: No register named %sl

  • %sl은 존재하지 않는 레지스터다.
  • 문제점: %sl이라는 레지스터는 존재하지 않음

movq %rax, $0x123

💬 답: Cannot have immediate as destination

  • $0x123는 즉시값immediate다.
  • 즉시값은 목적지가 될 수 없다. 즉시값은 항상 소스로만 사용된다.
  • 문제점: 즉시값은 목적지 오퍼랜드로 사용할 수 없음

movl %eax, %rdx

💬 답: Destination operand incorrect size

  • %eax는 32비트 레지스터, %rdx는 64비트 레지스터다.
  • movl 명령은 32비트 단위 복사를 수행하므로, 목적지도 32비트여야 한다.
  • %edx를 사용해야 맞다.
  • 문제점: 명령의 크기(l)와 오퍼랜드 크기 불일치 → %rdx는 64비트

movb %si, 8(%rbp)

💬 답: Mismatch between instruction suffix and register ID

  • %si는 16비트 레지스터다.
  • movb8비트(1바이트) 명령이다.
  • movb%si를 쓸 수 없고, 8비트 레지스터인 %sil이어야 한다.
  • 문제점: movb는 바이트 이동인데 %si는 워드(16비트) → 오퍼랜드 크기 불일치

3.4.3 데이터 이동 예제

🔹 바이트 이동 인스트럭션

다음 예제는 어떻게 데이터 이동 인스트럭션들이 목적지의 상위 바이트들을 변경하는지 안하는지를 보여준다. 세 개의 바이트 이동 인스트럭션들(movb, movsbq, movzbq)은 서로 미세한 차이점을 가진다.

movabsq	$0x0011223344556677, %rax	# %rax = 0011223344556677
movb	$0xAA, %dl			# %dl = AA
movb	%dl,%al				# %rax = 00112233445566AA
movsbq	%dl,%rax			# %rax = FFFFFFFFFFFFFFAA
movzbq	%dl,%rax			# %rax = 00000000000000AA
  • 코드의 첫 두 줄은 레지스터 %rax, %dl0011223344556677AA로 초기화한다. 나머지 인스트럭션들은 모두 %rdx의 하위 바이트를 %rax의 하위 바이트로 복사한다.
  • 3번째 줄의 movb 인스트럭션은 다른 바이트들을 변경하지 않는다.
  • 4번째 줄의 movsbq 인스트럭션은 다른 7바이트를 소스 바이트의 상위 비트에 따라 1이나 0으로 설정한다. 16진수 A가 이진수 1010을 나타내므로 부호 확장을 하면 상위 바이트들은 각각 FF로 세팅되도록 한다.
  • 5번째 줄의 movzbq 인스트럭션은 항상 다른 7바이트를 0으로 설정한다.

🔹 Exchange 루틴 코드

long exchange(long *xp, long y) {
    long x = *xp;
    *xp = y;
    return x;
}

exchange.c

# long exchange(long *xp, long y)
# xp in %rdi, y in %rsi
exchange:
    movq (%rdi), (%rax)
    movq %rsi, (%rdi)
    ret

exchange.s

  • 프로시저가 실행을 시작하면, 프로시저 매개변수 xpy는 레지스터 %rdi %rsi에 저장된다.
  • 첫 번째 movq 인스트럭션은 x를 메모리에서 읽어서 레지스터 %rax에 저장하며, 이것은 C 프로그램에서 x = *xp로 직접 구현된다. 후에 레지스터 %rax는 함수에서 값을 리턴할 때 사용되며, 그래서 리턴 값은 x다.
  • 두 번째 movq 인스트럭션은 y를 레지스터 %rdi에 저장된 xp로 지정한 메모리 위치에 써주며, 이것은 연산 *xp = y를 직접 구현한 것이다.

이 어셈블리 코드에서 두 가지 특징에 대해 주목해 볼 필요가 있다.

  • C언어에서 포인터라고 부르는 것이 어셈블리어에서는 단순히 주소다: 포인터를 역참조하는 것은 포인터를 레지스터에 복사하고, 이 레지스터를 메모리 참조에 사용하는 과정으로 이뤄진다.
  • x 같은 지역변수들은 메모리에 저장되기 보다는 종종 레지스터에 저장된다: 레지스터의 접근은 메모리보다 속도가 훨씬 빠르다.

 Practice 3.4                                                                                                                                  

변수 sp dp는 다음과 같은 자료형으로 선언되어 있다.

src_t *sp;
dest_t *dp;

우리는 적당한 두 개의 데이터 이동 인스트럭션을 이용해 다음의 연산을 구현하고자 한다.

*dp = (dest_t) *sp;

spdp 값들이 레지스터 %rdi %rsi에 저장되어 있다고 가정한다. 다음 표의 각 항목에 대해 지정된 데이터 이동을 구현하는 두 개의 인스트럭션을 보이시오. (드래그하면 정답이 보입니다.)

src_t dest_t Instruction Comments
long

long

movq (%rdi), %rax
movq %rax, (%rsi)
Read 8 bytes
Store 8 bytes
char

int

movsbl (%rdi), %eax
movl %eax, (%rsi)
Convert char to int
Store 4 bytes
char

unsigned

movsbl (%rdi), %eax
movl %eax, (%rsi)
Convert char to int
Store 4 bytes
unsigned char

long

movzbl (%rdi), %eax
movq %rax, (%rsi)
Read byte and zero-extend
Store 8 bytes
int

char

movl (%rdi), %eax
movb %al, (%rsi)
Read 4 bytes
Store low-order byte
unsigned

unsigned char

movl (%rdi), %eax
movb %al, (%rsi)
Read 4 bytes
Store low-order byte
char

short

movsbw (%rdi), %ax
movw %ax, (%rsi)
Read byte and sign-extend
Store 2 bytes
더보기

전제

  • sp 값은 %rdi, dp 값은 %rsi에 들어 있음.
  • *dp = (dest_t)*sp; 형태의 대입을 두 개의 명령어로 구현.
  • 첫 번째 인스트럭션: *sp를 적절히 확장해서 임시 레지스터에 로드
  • 두 번째 인스트럭션: 임시 레지스터에서 *dp 위치로 저장

해설

src_t dest_t Instruction 해설
long long movq (%rdi), %rax
movq %rax, (%rsi)
단순한 8바이트 복사. movq 사용하여 그대로 로드/저장
char int movsbl (%rdi), %eax
movl %eax, (%rsi)
char는 1바이트 signed이므로 movsbl 사용 (sign-extend byte to long). int는 4바이트이므로 저장은 movl.
char unsigned movsbl (%rdi), %eax
movl %eax, (%rsi)
여기서도 char는 signed이므로 movsbl로 sign-extend. unsigned는 4바이트지만 부호는 신경 안 써도 됨.
unsigned char long movzbl (%rdi), %eax
movq %rax, (%rsi)
unsigned char는 zero-extend 필요. movzbl로 바이트를 32비트로 확장, 저장은 movq로 8바이트 저장.
int char movl (%rdi), %eax
movb %al, (%rsi)
4바이트 int를 읽고 하위 8비트만 저장하므로 movb. 부호는 무시됨.
unsigned unsigned char movl (%rdi), %eax
movb %al, (%rsi)
위와 동일. 4바이트 unsigned에서 하위 1바이트만 저장.
char short movsbw (%rdi), %ax
movw %ax, (%rsi)
char(1바이트 signed)을 short(2바이트 signed)로 → sign-extend byte to word. movsbw 사용.

 

확장 명령어 요약

명령어 의미
movsbl sign-extend byte to long (1 → 4바이트)
movzbl zero-extend byte to long (1 → 4바이트)
movsbw sign-extend byte to word (1 → 2바이트)
movl 4바이트 이동
movq 8바이트 이동
movb 1바이트 이동
movw 2바이트 이동
더보기

%al, %ax는 둘 다 x86-64 아키텍처의 레지스터 %rax의 하위 부분이다. 간단히 정리하자면 다음과 같다.

 

%rax 계열 레지스터 분해

레지스터 크기 의미
%rax 64비트 전체 레지스터
%eax 32비트 하위 32비트 (상위 32비트는 자동으로 0으로 설정됨)
%ax 16비트 하위 16비트
%ah 8비트 하위 16비트 중 상위 8비트 (%ax의 상위 바이트)
%al 8비트 하위 8비트

 

예시 그림 (상위 → 하위 비트)

|       64-bit        |
|---------------------|
|        %rax         |

|     32-bit    |
|---------------|
|     %eax      | ← %rax의 하위 32비트

| 16-bit |
|--------|
|  %ax   | ← %eax의 하위 16비트

| 8-bit | 8-bit |
|-------|-------|
| %ah   | %al   | ← %ax의 상/하위 8비트

 

용도 예시

  • %al: 바이트 단위 연산할 때 (예: movb %al, (%rsi) → 1바이트 저장)
  • %ax: 워드(2바이트) 단위 연산할 때
  • %eax: 32비트 정수 연산에 주로 사용
  • %rax: 전체 64비트 값을 다룰 때 사용

참고

  • %eax에 값을 저장하면 상위 32비트는 자동으로 0이 된다. (예: movl $1, %eax → %rax = 0x00000001)
  • %ax, %al, %ah는 그보다 작은 범위만 바꾼다.

C언어에서 크기를 바꾸고, 부호화된 것을 변경하는 것과 관련된 캐스팅을 수행할 때에는 해당 연산이 크기를 먼저 변경해야 한다는 점을 기억해야 한다.

[C: 포인터 예제]
함수 exchange는 C에서의 포인터 사용을 잘 보여준다. 인자 xp는 long int의 포인터이고, y는 long integer이다. 다음 문장은 xp로 표시되는 위치에 저장된 값을 읽어서 지역변수 x에 저장한다는 것을 의미한다.
long x = *xp;​


이와 같은 읽기 연산을 포인터 역참조dereferencing라고 부른다. C 연산자 *는 포인터 역참조를 실행한다. 다음 문장은 정반대의 연산을 수행하는데, 매개변수 y의 값을 xp가 저장하는 위치에 쓴다.

*xp = y;​


이것도 또 다른 형태의 포인터 역참조 사례이다. 그러나 이 역참조는 할당문의 좌변에 위치하므로 쓰기 연산을 의미한다. 다음은 exchange함수의 사용 예이다.

long a = 4;
long b = exchange(&a, 3);
printf("a = %ld, b = %ld\n", a, b);


이 코드는 a = 3, b = 4를 출력한다. C 연산자 &는 (주소화 연산자라고 부르는) 포인터를 만들어주며, 이 예제에서는 지역변수 a를 저장하고 있는 위치의 주소를 생성한다. 함수 exchange는 a에 저장되어 있던 값을 3으로 덮어쓰지만, 함수값으로 이전 값인 4를 리턴해준다. exchange 함수에 어떻게 포인터를 전달하고, 특정 위치에 저장된 데이터를 수정할 수 있는지 살펴보자.

 

 Practice 3.5                                                                                                                                  

다음과 같은 프로토타입을 가지는 함수

void decode1(long *xp, long *yp, long *zp);

가 어셈블리 코드로 컴파일되어 다음의 결과를 생성하였다.

# void decode1(long *xp, long *yp, long *zp);
# xp in $rdi, yp in %rsi, zp in %rdx
decode1:
    movq (%rdi), %r8
    movq (%rsi), %rcx
    movq (%rdx), %rax
    movq %r8, (%rsi)
    movq %rcx, (%rdx)
    movq %rax, (%rdi)
    ret

매개변수 xy, yp, zp는 레지스터 %rdi, %rsi, %rdx에 각각 저장되어 있다. 위의 어셈블리 코드와 동일한 효과를 가지는 decode1을 C 코드로 작성하시오.

더보기

어셈블리 코드의 각 인스트럭션들은 다음의 의미를 가진다.

# void decode1(long *xp, long *yp, long *zp);
# xp in $rdi, yp in %rsi, zp in %rdx
decode1:
    movq (%rdi), %r8	# Get x = *xp
    movq (%rsi), %rcx	# Get y = *yp
    movq (%rdx), %rax	# Get z = *zp
    movq %r8, (%rsi)	# Store x at yp
    movq %rcx, (%rdx)	# Store y at zp
    movq %rax, (%rdi)	# Store z at xp
    ret

이것으로부터 다음과 같은 C코드를 작성할 수 있다.

void decode1(long *xp, long *yp, long *zp) {
    long x = *xp;
    long y = *yp;
    long z = *zp;
    
    *yp = x;
    *zp = y;
    *xp = z;
}

3.4.4 스택 데이터의 저장push과 추출pop

스택은 값들이 후입선출(LIFO)하는 형태로만 추가되거나 제거되는 자료구조이다. push 연산으로 스택에 데이터를 추가하고 pop 연산으로 제거하되, 제거하는 값이 가장 최근에 추가된 값이어야 하며, 그 값은 스택에 여전히 남아 있게 된다. 스택은 배열로 구현될 수 있으며, 배열에서 원소들의 배열의 한 쪽 끝에서만 추가하거나 제거한다. 이 끝부분을 스택의 top이라고 부른다.

스택연산 과정: 관습적으로 스택은 위아래를 뒤집어서 그리며, top은 맨 밑에 위치한다. x86-64에서 스택은 낮은 주소 방향으로 성장하며, push를 하게 되면 스택 포인터(%rsp)가 감소되고 메모리에 저장되며, pop을 하면 메모리에서 읽어내고 스택 포인터를 증가시킨다.

  • x86-64 에서는 프로그램 스택이 메모리의 특정 영역을 차지한다.
  • 스택의 top이 모든 스택 원소 중에서 가장 낮은 주소를 차지하고 있다.
  • %rsp 스택 포인터가 top 스택 원소의 주소를 저장하고 있다.
  • popq 인스트럭션이 데이터를 추출하는 반면, pushq 인스트럭션은 데이터를 스택에 추가하는 기능을 제공한다.
  • popqpushq 인스트럭션은 한 개의 오퍼랜드를 사용한다: 추가할 소스 데이터와 추출을 위한 데이터 목적지

쿼드워드 값을 스택에 추가하려면, 먼저 스택 포인터를 8 감소시키고, 그 값을 스택 주소의 새로운 top에 기록하는 것으로 구현된다. 따라서 pushq %rbp 인스트럭션의 동작은 다음과 동일하다.

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

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

 

쿼드워드를 pop하는 것은 스택 top 위치에서의 읽기 작업 후 스택 포인터를 8 증가시키는 것으로 구현된다. 따라서 popq %rax 인스트럭션의 동작은 다음과 동일하다.

movq (%rsp),%rax	# Read %rax from stack
addq $8,%rsp		# Increment stack pointer

 

위 그림에서 보는 것처럼 값 0x123은 다른 값이 덮어써질 때까지 메모리 주소 0x100에 여전히 남아있다. 그렇지만, 스택 탑은 언제나 %rsp가 가리키는 주소를 의미한다. 스택 top보다 윗부분에 저장된 값은 모두 무효인 값들이다.

 

스택이 프로그램 코드와 다른 형태의 프로그램 데이터와 동일한 메모리 주소에 저장되기 때문에, 프로그램들은 표준 메모리 주소지정 방법을 사용해서 스택 내 임의의 위치에 접근할 수 있다. 예를 들어, 스택 최상위 원소가 쿼드워드라면 movq 8(%rsp), %rdx 인스트럭션은 스택의 2번째 쿼드워드를 레지스터 %rdx에 복사해준다.