3강 프로그램의 기계수준 표현(1)

CSAPP 공부

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

컴퓨터 : 데이터를 처리하고, 메모리를 관리하고, 저장장치에 데이터를 읽거나 쓰고, 네트워크를 통해 통신하는 등의 하위 동작들을 인코딩한 연속된 바이트인 기계어 코드(machinc code)를 실행한다.

컴파일러 : 프로그램 언어의 규칙, 대상 컴퓨터의 인스트럭션 집합, 운영체제의 관례 등에 따라 기계어 코드 생성

GCC C 컴파일러 : 기계어 코드를 문자로 표시한 어셈블리 코드의 형태로 출력을 만들어 프로그램의 각 인스트럭션 생성

GCC : 어셈블러와 링커 호출 => 어셈블리 코드로부터 실행 가능한 기계어 코드 생성

어셈블리 코드 : 기계어 코드의 읽기 쉬운 형태

고급 언어(c, 자바) : 우리가 작성한 프로그램의 기계 수준 구현 드러나지 않음

어셈블리 코드 : 계산을 하기 위해 사용해야 하는 저급 인스트럭션 명시 필요

고급언어의 장점

  1. 안정성 : 고급 언어가 제공하는 높은 수준의 추상화 사용이 더 좋음(생산성도)

  2. 타입체크(from 컴파일러) : 프로그램 에러 검출, 균일한 데이터 참조,처리의 정확도 증가

  3. 다른 많은 컴퓨터에서 컴파일과 실행 가능

컴파일러를 적절한 커맨드라인 인자와 함께 호출 -> 컴파일러는 어셈블리 코드 형태의 파일로 출력 생성

=> 이 코드를 이해하면 컴파일러의 최적화 성능 파악 가능, 내재된 비효율성 분석 가능

최적화된 컴파일러 : 계산 (by C코드)의 실행 순서 조정, 불필요한 계산 제거, 느린 연산을 빠른 연산으로 교체,

재귀적 연산을 반복 연산으로 바꿈. => 역엔지니어링의 일종

역엔지니어링(reverse engineering) : 시스템(기계가 만들어낸 어셈블리 프로그램)이 만들어진 과정을 시스템을 연구하고 역방향으로 분석하여 이해하려는 작업

3.2 프로그램의 인코딩

C 프로그램을 두d 개의 파일 p1.c와 p2.c에 작성할 때, 유닉스 커맨드로 다음과 같이 컴파일함

linux> gcc -0g -o p p1.c p2.c

명령어는 GCC C 컴파일러를 지정. 리눅스에서 기본 컴파일러이기 때문에 간단히 cc로 호출 가능

커맨드 라인 옵션으로 -0g을 주면 컴파일러는 본래 C 코드의 전체 구조를 따르는 기계어 코드를 생성하는 최적화 수준을 적용한다.

일반적으로 최적화 수준 올리면 => 최종프로그램은 더 빨리 동작, but 컴파일 시간 증가, 디버깅 도구 실행 어려움

높은 수준의 최적화 적용 => 만들어진 코드가 너무 많이 변경되어 본래의 코드와 생성된 기계어 코드 간의 관계를 이해하기 힘듬

gcc 명령은 소스 코드를 실행 코드로 변환하기 위해 일련의 프로그램들을 호출

  1. C 전처리기가 #include로 명시된 파일을 코드에 삽입해 주고, #define으로 선언된 매크로를 확장해 준다.
  2. 컴파일러는 두 개의 소스파일의 어셈블리 버전인 p1.s와 p2.s를 생성한다.
  3. 어셈블러는 어셈블리 코드를 바이너리 목적코드인 p1.o와 p2.o로 변환한다.
  4. 목적코드는 기계어 코드의 한 유형이다(모든 인스트럭션의 바이너리 표현을 포함하고 있지만 전역 값들의 주소는 아직 채워지지 않았다.).
  5. 링커는 두 개의 목적코드 파일을 라이브러리 함수들을 구현한 코드와 함께 합쳐서 최종 실행파일인 p를 생성한다(명령줄 디렉티브 -op로 명시하여).
  6. 실행 코드는 우리가 다루게 될 두 번째 형태의 기계어 코드이다(이것이 프로세서가 실행할 정확한 코드의 형태).

3.2.1 기계수준 코드

컴퓨터 시스템은 보다 간단한 추상화 모델을 이용해서 세부 구현내용을 감추면서 추상화의 여러 가지 다른 형태를 사용

기계수준 프로그래밍에서 중요한 2가지

  1. 기계수준 프로그램의 형식과 동작은 인스트럭션 집합구조(instruction set architecture), 즉 “ISA”에 의해 정의된다. 이 ISA는 프로세서의 상태, 인스트럭션의 형식, 프로세서 상태에 대한 각 인스트럭션들의 영향들을 정의한다. 대부분의 ISA는 마치 하나의 인스트럭션이 다음 인스트럭션의 실행 전에 완료되는 순차적인 실행을 하는 것처럼 프로그램의 동작을 설명. 프로세서 하드웨어는 훨씬 정교해서 여러 인스트럭션을 동시에 실행하지만, ISA에 의한 순차적 동작과 일치하는 전체 동작을 보이도록 해주는 안전장치를 사용한다.

  2. 기계수준 프로그램이 사용하는 주소는 가상주소이며, 메모리가 매우 큰 바이트 배열인 것처럼 보이게 하는 메모리 모델을 제공한다. 실제 메모리 시스템은 여러 개의 메모리 하드웨어와 운영체제 소프트웨어로 구현되어 있음.

컴파일러 : 전체 컴파일 순서에서 C에서 제공하는 추상화된 실행모델로 표현된 프로그램을 프로세서가 실행하는 매우 기초적인 인스트럭션들로 변환하는 대부분의 일을 수행

어셈블리 코드 표현 : 기계어 코드와 매우 유사, 바이너리 기계어 코드 형식보다 읽기 쉬운 텍스트 형식

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

C : 다른 종류의 데이터 타입을 선언하고 메모리에 할당할 수 있는 모델 제공, 연결된 데이터 타입(배열,구조체)

기계어 코드 : 메모리를 단순히 바이트 주소지정이 가능한 큰 배열로 봄, 연속적인 바이트들로 표시, 스칼라(scalar) 데이터 타입의 경우 부호형과 비부호형, 다른타입의 포인터들, 포인터와 정수형 사이에도 구분 X

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

언제나 가상주소의 일부 제한된 영역만이 유효하다.

ex. x86-64 가상주소들은 64비트 워드들로 표현. 기계들의 현재 구현된 상태에서 상위 16비트는 0으로 지정, 주소는 2의 48승(256 테라 바이트) 범위를 넘어가는 바이트를 잠재적으로 명시할 수 있음. 운영체제는 이 가상 주소공간을 관리해서 가상주소를 실제 프로세서 메모리 상의 물리적 주소 값으로 번역해줌.

하나의 기계어 인스트럭션 : 매우 기초적인 동작 수행(ex. 레지스터들에 저장된 두 개의 수 더하기, 메모리와 레지스터 간에 데이터 교환, 새로운 인스트럭션 주소로 조건에 따라 분기)

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

3.2.2 코드 예제

C 코드 파일 mstore.c

long mult2(long, long);

void multstore(long x, long y, long *dest) {

long t = mult2(x,y);

*dest = t;

}

C 컴파일러가 생성한 어셈블리 코드를 보기 위해 명령줄에서 -s 옵션을 사용할 수 있음.

linux> gcc -0g -S mstore.c

GCC가 컴파일러를 실행하도록 해서 어셈블리 파일 mstore.s를 만들고, 더이상 진행 X(원래 그 다음에 어셈블러를 호출해서 목적코드 파일 생성)

어셈블리 코드 파일이 포함한 선언

mulstore:

pushq %rbx

movq %rdx, %rbx

call mult2

movq %rax, (%rbx)

popq %rbx

ret

위 코드의 각 라인은 하나의 기계어 인스트럭션에 대응

명령어 라인 옵션을 사용한다면 GCC는 코드를 컴파일하고 어셈블 할것.

linux> gcc -0g -c mstore.c

바이너리 형식이어서 직접 볼 수 없는 목적코드 파일 mstore.c를 생성.

다음은 14바이트의 16진수 데이터가 mstore.o 파일 1368바이트에 내장된 것

53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3

이것은 위에 나열된 어셈블리 인스트럭션에 대응되는 목적코드.

컴퓨터에 의해 실제 실행된 프로그램은 단순히 일련의 인스트럭션을 인코딩한 일련의 바이트

컴퓨터는 인스트럭션들이 생성된 소스 코드에 대한 정보를 거의 갖고 있지 않음.

기계어 코드 파일의 내용 조사 : 역어셈블러 프로그램.

역어셈블러 프로그램 : 기계어 코드로부터 어셈블리어 코드와 유사한 형태 생성.

리눅스에선 OBJDUMP에 -d 커맨드 라인 사용하여 가능.

linux> objdump -d mstore.o

결과

Disassembly of function multstore in binary file mstore.o

1 0000000000000000 <mulstore>:

offset byte equivalent assembly language

2 0 : 53 push %rbx

3 1 : 48 89 d3 mov %rdx,%rbx

44 : e8 00 00 00 00 callq 9 <mulstore+0x9

59 : 48 89 03 mov %rax, (%rbx)

6c : 5b pop %rbx

7d : c3 retq

왼쪽 : 앞에서 보았던 14개의 16진수 바이트 값, 1~5 바이트 그룹으로 나누어짐. 각 그룹은 하나의 인스트럭션으로 오른쪽에 어셈블리어와 동일

기게어 코드의 특징과 역어셈블된 표현

  • x86-64 인스트럭션들은 1에서 15바이트 길이. 인스트럭션 인코딩은 자주 사용되는 인스트럭션들과 오퍼랜드가 적은 것들이 짧은 길이를 갖도록 함. 반대는 더 긴 인스트럭션 길이를 갖도록 인코딩.
  • 인스트럭션의 형식은 주어진 시작 위치에서부터 바이트들을 기계어 인스트럭션으로 유일하게 디코딩 할 수 있도록 설계
  • 역어셈블러는 기계어 코드 파일의 바이트 순서에만 전적으로 의졶래서 어셈블리 코드를 결정(소스코드나 프로그램의 어셈블리 코드 버전 사용 X)
  • 역어셈블러는 GCC가 생성한 어셈블리 코드와는 약간 다른 명명법을 인스트럭션에 사용. 접미어에서 크기를 나타내는 ‘q’ 생략 가능. 역어셈블러는 ‘q’를 call과 ret 명령어에 붙임. 이 접미어들은 안전하게 생략 가능

실제 실행 가능 코드를 생성하기 위해서 링커를 목적코드들에 실행. 한 개는 main함수 포함. main.c파일에 다음 함수가 있다고 가정

#include <stdio.h>

void mulstore(long, long, long *);

int main(){

long d;

mulstore(2, 3, &d);

printf("2*3 --> %ld\n", d);

return 0;

}

long mult2(long a, long b) {

long s = a*b;

return s;

}

그 후, 실행 가능 프로그램 prog 생성 가능

linux> gcc -0g -o prog main.c mstore.c

파일의 크기가 8655바이트로 늘어난 이유 : 우리가 제공한 두 개의 프로시저뿐만 아니라 운영체제와 상호작용하기 위한 코드, 프로그램을 시작하고 종료하기 위한 코드들까지 포함해서

역어셈블

linux> objdum -d prog

추출한 여러 가지 코드

disassembly of function multstore in binary file prog

1 0000000000400540 <mulstore>:

2 400540: 53 mov push %rbx

3 400541: 48 89 d3 %rdx,%rbx

4 400544: e8 42 00 00 00 callq 40058b <mult2>

5 400549: 48 89 03 mov %rax, (%rbx)

6 40054c: 5b pop %rbx

7 40054d: c3 retq

8 40054e: 90 nop

9 40054f: 90 nop

이 코드는 mstore.c를 역어셈블해서 생성한 것과 거의 동일.

중요한 차이

  1. 왼쪽에 나타낸 주소가 다름(링커가 이 코드의 위치를 다른 주소영역으로 이동)

  2. 링커가 callq 인스트럭션이 함수 mult2를 호출할 때(역어셈블된 코드의 4번 줄) 사용해야 하는 주소를 채움

링커의 임무 : 이들 함수들을 위한 실행 코드의 위치들과 함수 호출 일치시키는 것

  1. 두 줄의 추가된 라인을 볼 수 있는것(8~9번 줄).

이 인스트럭션들은 7번줄의 리턴 이후에 발생해서 프로그램에는 효과 X 함수를 위한 코드 길이를 16바이트로 늘려 코드의 다음 블록을 메모리 시스템 성능면에서 더 잘 배치하기 위해 삽입.

3.2.3 형식에 대한 설명

GCC가 생성하는 어셈블리 코드 : 사람이 읽기 어려움

linux> gcc -0g -S mstore.c

이 파일의 전체 내용

.file "010-mstore.c"

​ ` .text`

.global multstore

.type multstore, @function

multstore:

pushq %rbx

movq %rdx, %rbx

call mult2

movq %rax, (%rbx)

popq %rbx

ret

.size multstore, .-multstore

.ident "GCC: (Ubuntu 4.8.1-2ubuntu1~12.04) 4.8.1"

.section .note.GNU-stack,"",@progbits

’.’으로 시작하는 모든 라인은 어셈블러와 링커에 지시하기 위한 디렉티브(directive). 일반적으로 무시. 반면, 인스트럭션들이 무엇을 하고, 이들이 어떻게 소스 코드와 연관되는지에 대한 설명 X

라인번호, 주석 포함한 버전

*void mulstore(long x, long y, long *dest)*

x in %rdi, y in %rsi, dest in %rdx

1 mulstore:

2 pushq %rdx, %rbx save %rbx

3 movq %rdx, %rbx Copy dest to %rbx

4 call mult2 Call mult2(x,y)

5 movq %rax, (%rbx) *Store result at *dest*

6 popq %rbx Restore %rbx

7 retReturn

주석은 각 인스트럭션의 효과와 그것이 어떻게 원본 C코드와 연관되는지 간략한 내용

3.3 데이터의 형식

워드 : 16비트 데이터 타입

더블워드 : 32비트의 양

쿼드워드 : 64비트의 양

기본 데이터 타입에 사용되는 X86-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
doulbe Double precision l 8

부동소수점 숫자의 두 기본 형태

  1. C의 float 타입에 대응되는 단일정밀도(4바이트) 값
  2. C의 double 타입에 대응되는 이중 정밀도(8바이트) 값

x86 계열 마이크로프로세서는 80비트 부동소수점 형식 연산 구현 => 비사용 추천( 다른 머신 호환, 고성능의 하드웨어 구현 X)

단일문자 접미어

  • movb(바이트 이동)
  • movw(워드 이동)
  • movl(더블 워드 이동) -> 32비트 양이 long word로 간주되기 때문
  • movq(쿼드워드 이동)

접미어 ‘l’은 4바이트 정수 , 8바이트 더블 정밀도 부동소수점 수에도 사용

3.4 정보 접근하기

x86-74 주처리장치 CPU : 64비트 값을 저장할 수 있는 16개의 범용 레지스터 보유 => 정수 데이터와 포인터를 저장하는 데 사용

인스트럭션 : 16개의 레지스터 하위 바이트들에 저장된 다양한 크기의 데이터에 대해 연산 가능

바이트수준 연산 : 가장 덜 중요한 바이트에 대해 연산 (16비트 연산 -> 2바이트, 32비트 연산 -> 4바이트, 64 -> 레지스터 전체)

이 인스트럭션들이 레지스터들을 목적지로 할 때에는 8바이트보다 작은 바이트를 생성하는 인스트럭션들의 레지스터에서 남는 바이트들에 대해 어떻게 처리하는가

  1. 1 또는 2바이트를 생성하는 경우에는 나머지 바이트들은 변경 없이 그대로 유지된다.
  2. 4바이트 길이의 값을 생성하는 경우는 상위 4바이트를 0으로 설정한다.

일반적인 프로그램 : 서로 다른 레지스터들은 서로 다른 목적으로 이용, 몇 개의 인스트럭션들은 특정 레지스터들에 국한되어 사용

일련의 표준 프로그래밍 관습에 의해 스택을 관리하고, 함수의 인자를 넘겨주고, 함수에서 값을 리턴하고, 로컬 데이터와 임시 데이터를 저장하기 위해 어떻게 레지스터가 사용되는지가 정해진다.

3.4.1 오퍼랜드 식별자(specifier)

대부분의 인스트럭션 : 하나 이상의 오퍼랜드가짐

오퍼랜드 : 연산을 수행할 소스(source)값과 그 결과를 저장할 목적지(destination)의 위치를 명시

소스값 : 상수로 받기 or 레지스터,메모리로부터 읽어서

여러 가지 오퍼랜드의 종류

  1. immediate : 상수값, ATT 형식의 어셈블리 코드에서 ‘$’기호 다음에 C 표준 서식을 사용하는 정수($-577, $0x1F)
  2. register : 레지스터의 내용, 각각 16개의 64,32,16,8비트 레지스터들의 하위 일부분인 8,4,2,1,바이트 중 하나의 레지스터 가리킴.
  3. 메모리 참조:유효주소(effective address)라고 부르는 계산된 주소에 의해 메모리 위치에 접근하게 된다. 메모리는 거대한 바이트의 배열로 볼 수 있으므로 Mb[Addr]과 같이 표시

주소지정방식 : 여러 가지 유형의 메모리 참조를 가능하게 하도록 많이 존재함

일반적인 형태 : Imm(rb,ri,s), 상수 오프셋(Imm), 베이스 레지스터(rb), 인덱스 레지스터(ri), 배율 s(1,2,4,8의 값 중 하나)

기본 레지스터, 인덱스 레지스터 : 64비트 레지스터

유효주소 : Imm + R[rb] + R[ri]*s

3.4.2 데이터 이동 인스트럭션

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

-MOV 클래스 : 이 인스트럭션들은 소스 위치에서 데이터를 목적지 위치로 어떤 변환도 하지 않고 복사. 4개의 인스트럭션으로 구성(movb, movw, movl, movq) 차이는 서로 다른 크기의 데이터에 대해 계산한다는 점(1,2,4,8바이트)

아래는 간단한 데이터 이동 인스트럭션

Instruction   Effect Description
MOV S,D D <- S Move
movb     Move byte
movw     Move word
movl     Move double word
movq     Move quad word
movabsq I, R R <- I Move absolute quad word

소스 오퍼랜드 : 상수, 레지스터 저장 값, 메모리 저장 값을 표시

목적 오퍼랜드 : 레지스터 또는 메모리 주소의 위치를 지정

in x86-64 : 데이터 이동 인스트럭션에서 두 개의 오퍼랜드 모두가 메모리 위치에 올 수 없도록 제한

복사를 위한 두개의 인스트럭션 필요 : 1. 소스 값을 레지스터에 적재하는 인스트럭션 2. 이 레지스터의 값을 목적지에 쓰기 위한 인스트럭

잍스트럭션들의 레지스터 오퍼랜드 : 레지스터 16개 중에서 이름을 붙인 부분이 될 수 있음, 크기는 마지막 문자(‘b’,’w’,’l’,’q’)가 나타내는 크기와 일치해야 한다.

대부분의 경우 MOV 인스트럭션 : 특정 레지스터 바이트들이나 대상 오퍼랜드에 의해 지정된 메모리 위치만을 업데이트

(예외. movl이 레지스터를 목적지로 갖는 경우 : 레지스터의 상위 4바이트도 0으로 설정)

예제 : 다섯 가지 가능한 조합의 소스와 목적지 유형. 소스 오퍼랜드가 먼저 나오고 목적 오프랜드가 나온다.

1
2
3
4
5
1 movl $0x4050,%eax	    # immediate--Register, 4 bytes
2 movw %bp,s%p          # Register--Register,  2 bytes
3 movb (%rdi,%rcx),%al  # Memory--Register,    1 byte
4 movb $-17,(%rsp)      # Immidiate--Memory,   1 byte
5 movq %rax,-12(%rbp)   # Register--Memory,    8 bytes

레지스터 movq 인스트럭션 : 32비트 2의 보수 숫자로 나타낼 수 있는 상수 소스 오퍼랜드들만 갖음. 그 후 부호 확장되어 목적지를 위해 64비트 값을 생산

movabsp 인스트럭션: 임의의 64비트 상수 값을 소스 오퍼랜드로 가질 수 있음. 목적지로는 레지스터만 가능

위보다 작은 소스 값을 더 큰 목적지로 복사할 때 사용하기 위한 두 종류의 데이터 이동 명령: 모두 레지스터나 메모리에 저장되어 있는 소스로부터 레지스터 목적지로 복사한다.

MOVZ 클래스의 인스트럭션 : 목적지의 남은 바이트들을 모두 0으로 채워줌

MOVS 클래스 의 인스트럭션 : 목적지의 남은 바이트들을 소스 오퍼랜드의 가장 중요한 비트를 반복해서 복사하는 부호 확장으로 채움

위 명령들의 이름 : 마지막 두개의 문자가 크기를 나타내는 지시자를 가짐 ( 첫번째는 소스의 크기, 두번째는 목적지의 크기)

각각 세 개의 인스트럭션 포함, 1,2바이트 소스 크기와 2,4바이트 목적지 크기 지원

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

Instruction   Effect Description
MOVZ S,R R <- ZeroExtend(S) Move with zero extension
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

4바이트 소스 값->8바이트 목적지(double word to quad word?)로 0으로 확장하는 명확한 인스트럭션 X

레지스터를 목적지로 하는 movl 인스트럭션을 이용해 구현

cltq 인스트럭션 : 오퍼랜드X, 언제나 레지스터 %eax : 소스, %rax : 목적지 사용해서 부호 확장 결과 만듬. => movslq, %eax, %rax와 동일환 효과지만 더 압축적인 인코딩 가짐.

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

Instruction   Effect Description
Move S,R R <- SignExtend(S) move with sign extension
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   %rax <- SignExtend(%eax) Sign-extend %eax to %rax

3.4.3 데이터 이동 예제

exchange 루틴을 위한 C(위)와 어셈블리(아래)코드.

1
2
3
4
5
6
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 	moveq	(%rdi) , %rax	#Get x at xp. Set as return value.
3 	moveq	%rsi,	(%rdi)	#Store y at xp.
4 	ret						#Return.

어셈블리 코드에서 볼 수 있듯 함수 exchange는 단 세 개의 인스트럭션으로 구현되었다. 두 개의 데이터 이동(movq), 그리고 함수가 호출된 위치로 리턴하는 인스트럭션 한개(ret). 인자들이 레지스터로 함수에 전달된다.

설명을 추가한 어셈블리 코드의 내용은 다음과 같다. : 리턴 값을 레지스터 %rax에 저장해서 함수가 값을 리턴하거나, 이 레지스터의 하위 부분 중의 하나로 리턴한다.

프로시저가 실행을 시작하면, 프로시저 매개변수 xp와 y는 레지스터 %rdi와 %rsi에 저장된다. 인스트럭션 2는 x를 메모리에서 읽어서 레지스터 %rax에 저장하며, 이것은 C 프로그램에서 x = *xp 로 직접 구현된다. 나중에 레지스터 %rax는 함수에서 값을 리턴할 때 사용될 것이며, 그래서 리턴 값은 x다. 인스트럭션 3은 y를 레지스터 %rdi에 저장된 xp로 지정한 메모리 위치에 써주며, 이것은 연산 *xp = y를 직접 구현한 것이다. 이 예제는 어떻게 MOV 인스트럭션을 이용해서 메모리에서 레지스터로 읽어들이는지(2번 줄), 레지스터에서 메모리로 쓰는지(3번 줄)를 보여준다.

위 어셈블리 코드의 두가지 특징

  1. C언어에서 “포인터”라고 부르는 것이 어셈블리어에서는 단순히 주소(포인터를 역참조 => 포인터를 레지스터에 복사, 메모리 참조에 사용)
  2. x 같은 지역변수들은 메모리에 저장되기보다는 종종 레지스터에 저장된다(레지스터의 접근이 메모리보다 빠름)

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

push와 pop 인스트럭션

Instruction Effect Description
pushq S R[%rsp] <- R[%rsp]-8; push quad word
  M[R[%rsp]] <- S  
popq D D <- M[R[%rsp]]; pop quad word
  R[%rsp] <- R[%rsp] + 8  

스택은 프로시저 호출을 처리하는 데 중요한 역할. 스택은 값들이 후입선출로 추가,제거 되는 자료구조.

원솧들이 추가되거나 제거되는 한쪽 끝을 스택의 top이라 부름.

스택의 top 원소가 모든 스택 원소 중 가장 낮은 주소를 갖는 형태(위에서 아래로 성장, 관습적으로 그렇게 그림)

스택 포인터 %rsp는 스택 맨 위 원소의 주소를 저장

popq 인스트럭션 : 데이터 추출

pushq 인스트럭션 : 데이터를 스택에 추가

두 인스트럭션은 한 개의 오퍼랜드 사용(추가할 소스 데이터와 추출을 위한 데이터 목적지)

쿼드워드 값을 스택에 추가하기

  1. 스택 포인터를 8 감소시킨다.
  2. 그 값을 스택 주소의 새로운 top에 기록

즉, pushq %rbp 인스트럭션의 동작은 다음과 같음

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

아래는 총 8바이트 필요

pushq는 한 바이트의 기계어 코드로 인코딩

Initially 초기상태
%rax 0x123
%rdx 0
%rsp 0x108
stack “top” : 0x108  
Initially 초기상태
%rax 0x123
%rdx 0
%rsp 0x100
stack “top”:0x100 , 0x123 stack에 추가되고 그 밑이 top이 됌  
Initially 초기상태
%rax 0x123
%rdx 0x123
%rsp 0x108
stack “top” : 0x108, 0x123이 pop되면서 top이 원래자리로 돌아감.  

첫번째 표는 %rsp가 0x108이고 %rax가 0x123일 때 pushq %rax를 실행한 결과를 보여줌

%rsp는 8 감소해서 0x100이 되고, 0x123이 메모리 주소 0x100에 저장됨

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

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

세번째 표는 pushq 실행 직후에 popq %rdx 인스트럭션을 실행한 결과. 값 0x123을 메모리에서 읽어서 레지스터 %rdx에 기록.

레지스터 %rsp는 다시 0x108로 복구.

하지만 값 0x123은 다른 값이 덮어써질 때까지 메모리 주소 0x100에 여전히 남아 있음(다른 push등으로 가능)

스택 top는 언제나 %rsp가 가리키는 주소를 의미. ( 그 윗부분 값은 무효)

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

3.5 산술연산과 논리연산