본문 바로가기
Computer Systems

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

by NOHCODING 2022. 11. 6.
반응형

00. 어셈블리어를 공부해야하는이유

 컴퓨터는 데이터를 처리, 메모리 관리, 저장장치에 데이터를 읽거나 쓰고 네트워크를 통해 통신하는 등 많은 일을 하고 있다. 다양한 동작들을 수행하려면 인코딩한 연속된 바이트 기계어 코드가 실행된다. 컴파일러는 다양한 동작들을 인코딩한 기계어 코드로 생성한다. 

 

  GCC C 컴파일러는 유닉스/리눅스 계열 플랫폼의 표준 컴파일러이다. 기계어 코드를 문자로 표시한 어셈블리 코드의 형태로 출력을 만들어 각 프로그램의 인스트럭션을 만들어낸다. 그리고 어셈블러와 링커를 호출하여 어셈블리어로부터 실행 가능한 기계어 코드를 생성한다.

 

자바를 포함해서 C같은 고급언어로 프로그램을 할 때, 우리가 작성한 프로그램이 기계수준에서 어떻게 구현되어 있는지 잘 드러나지 않는다. 반면, 어셈블리 코드로 프로그램을 짤 때는 프로그래머가 계산을 하기 위해 사용해야하는 저급 인스트럭션을 명시해야한다. 또, 어셈블리 코드는 컴퓨터 기계에 매우 의존적이다.

 

그럼에도 어셈블리어를 배워야 하는 이유는 존재한다.  첫째, 고급 언어를 사용하면서 추상화 계층 때문에 이해가  필요한 프로그램의 런타임 동작이 감춰지는 경우도 종종 있다. (ex. 쓰레드 패키지를 이용해서 동시성 프로그램을 작성할 때 프로그램의 데이터 공유 방법, 쓰레드들이 private로 유지하는 방법, 공유된 데이터가 정확히 어디서, 어떻게 접된 되는지) 둘째, 악성 프로그램이 시스템을 감염시킬 수 있는 프로그램을 공격하는 방법 중 상당 수가 프로그램이 런타임 제어 정보를 저장하는 방식의 미묘한 차이와 관련이 많다. 

  👉 32비트 : 2^32램 사용
  👉 64비트 : 2^48  ~ 2^64램 사용
  👉 x84-64는 IA32의 64비트 확장형

 

01. 프로그램의 인코딩

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

  2. 컴파일러는 두개의 소스파일의 어셈블리 버전인 p1.s와 p2.s를 생성한다.

  3. 어셈블러는 어셈블리 코드를 바이너리 목적코드인 p1.o와 p2.o를 생성한다.

  4. 링커는 두 개의 목적코드 라이브러리 함수들을 구현한 코드와 함께 합쳐 최종 실행파일인 p를 생성한다.  

목적코드 : 기계어 코드의 한 유형

 (1) 기계어 수준 코드 

  컴퓨터 시스템은 추상화 모델을 이용해서 세부구현을 감추면서 추상화의 여러 가지 다른 형태를 사용하고 있다.  추상화 중 두가지가 기계수준 프로그래밍에서 가장 중요하다.

 추상화가 중요한 이유 중 첫번째는 기계수준 프로그램의 형식과 동작은 인스트럭션 집합구조(ISA)에 의해 정의된다. 둘째는 기계수준프로그래밍이 사용하는 주소는 가상주소이며, 메모리가 매우 큰 바이트 배열인 것 처럼 보이게 하는 메모리 모델을 제공한다.  

ISA : 프로세서의 상태, 인스트럭션의 형식, 프로세서 상태에 대한 각 프로세서임

 

 

02. 데이터의 형식

인텔 프로세서들이 근본적으로 16비트를 사용하다가 추후에 32비트로 확장했기 때문에 인텔은 워드라는 단어를 16비트 데이터 타입을 말할 때 사용한다. 64비트의 양은 "쿼드워드"라고 한다. 포인터는 항상 8byte를 유지한다.

C declataion Intel data Type Assembly-code suffix Size(bytes)
char Byte b 1
short Word w 2
int Double word(32bit) l 4
long Quad word(64bit /  8yte) q 8
char* Quad word q 8
float Single precision s 4
double Double precision l 8

 

GCC가 생성한 대부분의 어셈블리 코드 인스트럭션들은 오퍼랜드의 크기를 나타내는 단일문자 접미어를 가지고 있다.  접미어 'l'은 더블워드의 경우에만 사용되는데 그 이유는 비트의 양이 "long word"로 간주되기 때문이다. 

함수 표현
movb 바이트 이동
movw 워드 이동
movl 더블워드 이동
movq 쿼드워드 이동

 

03. 정보 접근하기

 x86-64 주처리장치 cpu는 64비트 값을 저장할 수 있는 16개의 범용 레지스터를 보유하고 있다. 아래 그림은 레지스터이며 레지스터는 정수데이터와 포인터를 저장하는데 사용한다.  전체 16개 레지스터의 하위바이트들은 바이트(b), 워드(W / 16bit), 더블워드(32비트), 쿼드워드씩 접근할 수 있다. 

 

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

 일반적인 프로그램에서 서로 다른 레지스터들은 각각의 다른 목적으로 사용된다. 그 중 %rsp는 스택포인터로 런타임 스택의 끝 부부을 가리키기 위해 사용된다.  %rip는 프로그램 카운터라고도 하며, 실행할 다음 인스트럭션의 메모리 주소를 가리킨다.

 

몇 개의 인스트럭션들은 특정 레지스터에 국한 되어 사용되는데, 관습에 의해 스택을 관리하고, 함수의 인자를 넘겨주고, 함수에서 값을 리턴하고, 로컬 데이터와 임시 데이터를 저장하기 위해 어떻게 레지스터가 사용되는지가 정해진다. 

 

00. 오퍼랜드 식별자(specifier)

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

 

첫째, immediate로 상수 값을 말한다. ATT 형식의 어셈블리 코드에서 상수는 '$' 기호 다음에 사용한다. 어셈블러는 값을 인코딩하는 가장 컴팩트한 방법을 자동으로 선택하게 된다.

 

 둘째, register는 레지스터의 내용을 나타내며, 16개의 64비트, 32비트, 8비트 레지스터의 하위 일부분인 8,4,2,1 바이트 중 하나의 레지스터를 가리킨다. 

 

셋째, 메모리 참조로 유효주소라고 부르는 계산된 주소에 의해 메모리 위치에 접근하게 된다. 메모리는 거대한 바이트의 배열로 생각할 수 있다. 아래 그림처럼 여러가지 유형의 메모리 참조를 가능하게 하는 많은 주소 지정방식이 존재한다.

반응형

댓글