[CSAPP] 3. 프로그램의 기계수준 표현 1

이도윤·2022년 11월 25일
0

0. 서론

컴퓨터는 데이터를 처리하고, 메모리를 관리하고, 저장장치에 데이터를 읽거나 쓰고, 네트워크를 통해 통신하는 등의 하위 동작들을 인코딩한 연속된 바이트인 기계어 코드를 실행한다.
컴파일러는 프로그램 언어의 규칙, 대상 컴퓨터의 인스트럭션 집합, 운영체제의 관례 등에 따라 기계어 코드를 생성한다.
어셈블러 코드로 프로그램을 짤 때는 프로그래머가 계산을 하기 위해 사용해야 하는 저급 인스트럭션들을 명시해야 한다.
기계어 코드를 배우면 컴파일러의 최적화 성능을 알 수 있으며, 코드에 내제된 비효율성을 분석할 수 있다.
이 장은 X86-64에 기초하고 있다.




1. 역사적 관점

x86이라고 불리는 Intel 프로세서 라인은 긴 발전적인 개발이 있었다.
높은 성능과 더 발전된 운영체제를 지원하기 위한 요구에 맞게 기술적인 향상에 이점을 띄었다.

무어의 법칙(Moore's Law)
반도체칩 기술의 발전속도에 관한 것으로, 반도체칩에 집적할 수 있는 트랜지스터의 숫자가 18개월마다 두배씩 증가한다는 법칙




2. 프로그램의 인코딩

p1.c와 p2.c라는 파일을 생성하고 유닉스 명령어 줄에서 컴파일하려면:

_$ gcc -Og -o p p1.c p2.c_

gcc는 GCC C 컴파일러를 가리킨다.

C 언어에서 gcc 명령은 소스 코드(test.c)를 실행 코드로 변환하기 위해 일련의 프로그램들을 호출한다.

  1. C 전처리가 #include로 명시된 파일을 코드에 삽입해 주고 #define으로 선언된 매크로를 확장해준다.

  2. 컴파일러는 소스파일의 어셈블러 버전(test.s)를 생성한다.

  3. 어셈블러는 어셈블리 코드를 바이너리 목적코드인(test.o)로 변환한다.
    - 목적코드는 기계어 코드의 한 유형이다. 모든 인스트럭션과 바이너리 표현을 포함하고 있지만 전역 값들의 주소는 아직 채워지지 않았다.

  4. 마지막으로 링커가 목적코드 파일을 라이브러리 함수들을 구현한 코드와 함께 합쳐서 최종 실행 파일인 p를 생성한다.

커맨드 라인 옵션으로 -0g를 주면 C 코드의 전체 구조를 따르는 기계어 코드를 생성하는 최적호 수준을 적용한다.
높은 수준의 최적화를 적용하면 만들어진 코드가 너무 많이 변경되어 본래의 코드와 생성된 기계어 코드 간의 관계를 이해하기 어렵다.


2.1 기계수준 코드

컴퓨터 시스템은 다양한 다른 형태의 추상화를 사용한다.
기계 수준 프로그래밍에서 두 가지가 특히 중요하다.

첫 번째, 기계 수준 프로그램의 형식과 동작은 명령어 집합 구조(instruction set architecture: ISA)에 의해 정의된다.
프로세서 상태, 명령어의 형식를 정의하고 각각의 명령어가는 상태에 미치는 효과를 정의한다.
프로세서는 많은 명령어를 동시에 실행하려고 매우 정교한 하드웨어이지만, 모든 동작을 ISA에 의해 설명된 순차적인 연산을 매치하는 것을 확실하게 하기 위해 보호 수단을 사용한다.

두 번째, 기계 수준 프로그램에 의해 사용된 메모리 주소는 가상 주소이다.
메모리 시스템의 실제 구현은 다양한 하드웨어 메모리와 운영 체제 소프트웨어의 결합과 연관이 있다.

어셈블리 코드 표현은 기계 코드와 매우 비슷하다.

기계 코드의 이진 형태보다는 읽을 수 있는 형태로 되어 있다.

C가 메모리에 선언되고 할당되는 다른 데이터 타입의 오브젝트 모델을 제공해주는 반면, 기계 코드는 메모리를 큰 바이트-어드레스할 수 있는 배열이다.

프로그램 메모리는 프로그램에 대한 실행 가능한 기계 코드와 운영 체제에 의해 요구된 정보, 프로시저 호출과 반환를 관리하는 런타임 스택, 사용자에 의해 할당된 메모리 블럭을 포함한다.

프로그램 메모리는 가상 주소를 사용하여 어드레스된다.

운영체제는 가상 주소 공간을 관리하고, 가상 주소를 실제 프로세서 메모리에 있는 물리 주소로 번역한다.


2.2 코드 예제

아래의 코드로부터 시작한다.

#include <stdio.h>

long mult2(long, long);

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

$ gcc -Og -S mstore.c 명령어로 컴파일하면 mstore.s 어셈블리 파일이 생긴다.

movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret

기계에 의해 실행된 프로그램은 명령어 시리즈로 인코딩된 간단한 바이트 시퀀스이다.

기계 코드 파일의 내용을 보려면, 디셈블러(disassembler)라고 불리는 프로그램이 중요하다.
이 프로그램은 기계 코드로부터 어셈블리 코드와 비슷한 형태를 생성한다.
리눅스 시스템에서, OBJDUMP 프로그램은 -d 플래그와 함께 제공한다.

$ objdump -d mstore.o

	Disassembly of function sum in binary file mstore.o
1 	0000000000000000 <multstore>:
    Offset 	Bytes 				Equivalent assembly language
2 	0: 		53 					push 	%rbx
3 	1: 		48 89 d3 			mov 	%rdx,%rbx
4 	4: 		e8 00 00 00 00 		callq 	9 <multstore+0x9>
5 	9: 		48 89 03 			mov 	%rax,(%rbx)
6 	c: 		5b 					pop 	%rbx
7 	d: 		c3 					retq

왼쪽에는 앞에 보여준 14개의 16진수 바이트를 볼 수 있으며 1 ~ 5바이트 그룹으로 나누었다.

책에서는 ‘53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3’ 시퀀스를 0: 1: 4: 9: c: d: e:으로 나누었다.
각각의 그룹은 오른쪽에 보여진 어셈블리어와 같은 단일 명령어이다.




profile
Java 백엔드 개발자

0개의 댓글