Fundamental of CS/: : CSAPP

[CSAPP] Ch 3. 프로그램의 기계수준 표현 : (2) 프로그램의 인코딩

Jay.P Morgan 2024. 3. 19. 23:21

  3.2  프로그램의 인코딩

 

  GCC컴파일러를 통해 컴파일을 진행하면(커맨트 라인 옵션으로 -Og) 최적화 수준을 적용한다.

  일반적으로 최적화 수준을 올리면 최종 프로그램은 더 빨리 동작하지만, 컴파일 시간이 증가하고 디버깅 도구를 실행하기 어려워질 위험이 있으며 만들어진 코드가 기존의 C코드에 비해 너무 많이 변경되어 본래 코드와 기계어 코드간의 관계를 이해하기 어렵게된다.

 

  이러한 GCC 명령은 소스코드를 실행코드로 변환하기 위해 일련의 프로그램들을 호출한다. 앞서 1장에서 보았듯이, 전처리기, 컴파일러, 어셈블러, 링커까지 호출하게 된다.

 

 

  3.2.1  기계수준 코드

 

  컴퓨터 시스템은 보다 간단한 추상화 모델을 이용해서 세부구현내용은 감추며 추상화의 다른 형태를 사용하고 있다. 이들 중 "ISA"와 "가상메모리"가 기계수준 프로그래밍에서 특히 중요하다.

 

  인스트럭션 집합구조(ISA, instruction set architecture) : 기계수준 프로그램의 형식과 동작 등을 정의한다.

      프로세서의 상태 (C 프로그래머에게는 일반적으로 감춤)

      인스트럭션의 형식

      프로세서 상태에 대한 각 인스트럭션들의 영향들

x86-64 포함하여 대부분의 ISA 마치 하나의 인스트럭션이 다음 인스트럭션이 실행되기 전에 완료되는  처럼 순차적인 동작 설명한다.

 

  가상 메모리

   기계수준 프로그램이 사용하는 주소는 가상주소이며, 메모리가 매우 큰 바이트 배열인 것처럼 보이게 하는 메모리 모델을 제공한다.

  레지스터의 종류:

      프로그램카운터

         - PC라고 하며, x86-64에서는 %rip 표현 (RIP)

      정수 레지스터 파일

         - 64비트의 값을 저장하기 위한 16개의 이름을 붙인 위치를 갖는다.

       - 주소나 정수데이터를 저장할 수 있다.

       - 일부 레지스터는 프로그램의 중요한 상태를 추적하는데 사용

       - 다른 레지스터들은 함수의 리턴값 뿐만 아니라 프로시저의 지역변수와 인자 같은 임시값을 저장

      조건코드 레지스터

         - 가장 최근에 실행한 산술 또는 논리 인스트럭션에 관한 상태정보를 저장한다.

         - if나 while문을 구현할 때 필요한 제어나 조건에 따른 데이터 흐름의 변경을 구현하기 위해 사용

      벡터 레지스터

         - 하나 이상의 정수나 부동소수점 값들을 각각 저장할  있다.

 

C가 다른 종류의 데이터 타입을 선언하고 메모리에 할당할 수 있는 모델을 제공하는 반면,

기계어 코드는메모리를 단순히 바이트 주소지정이 가능한 큰 배열로 본다. 

C언어에서 배열과 구조체같은 연결된 데이터타입들은 기계어 코드로 번역될때, 연속적인 바이트들로 표시된다.

어셈블리코드는 부호형과 비부호형, 다른 타입의 포인터들, 심지어 포인터와 정수형 사이에서도 구분을 두지 않는다!

(포인터를 담는 레지스터가 정수를 담기도 하고,  반대도 성립한다.)

 

프로그램 메모리는 실행 기계어 코드, 운영체제를 위한 일부 정보, 런타임 스택(프로시저 호출과리턴을 관리), 힙 메모리 블록들을 포함하고 있다. 그리고 언제나 가상주소의 일부 제한된 영역만이 유효하다.

  x86-64에서의 가상주소들은 64비트 워드들로 표현된다. (상위 16비트는 0으로 지정되고 나머지는 48비트로 표현된다.) 운영체제는  가상 주소공간들을 관리하여 가상주소를 실제 프로세서 메모리상의 물리적 주소값으로 번역한다.

 

 

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

  (레지스터들에 저장된 두 수를 더하고, 메모리와 레지스터 간에 데이터를 교환하거나, 새 인스트럭션 주소로 분기 등)

 따라서 우리가 복잡하게 코드를  것들도 결국엔 간단한 인스트럭션을 조합하여 풀이되게 된다.

 

  3.2.2  코드 예제

 

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

linux$ gcc -Og -S *****.c

 

※ GCC를 지원하는 오픈소스 커뮤니티는 SoC업체에서 제공하는 변경된 코드 가이드라인에 따라 보다 효율적인 코드를 생성하려고 코드 생성기를지속적으로 수정하고 있다. 

 

  바이너리 형식의 목적코드 파일을 열어보면 이런형태를 나타낸다.

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

  이 상태가 우리가 말하는 기계어(목적코드). 결국 컴퓨터에 의해 실제 실행되는 프로그램은, 단순히 일련의 인스트럭션을 인코딩한 바이트들이다.

 

  기계어 코드 파일의 내용을 조사하려면, 역어셈블러라고 하는 프로그램이 매우 중요하다. 리눅스 시스템에서 OBJDUMP 프로그램 에 -d 커맨드 라인을 사용하면 이 역할을 수행할 수 있다. (기계어 코드 → 어셈블리어 코드와 유사한 형태)

linux> objdump -d *****.o

 

 

 

  분명 위에서는 목적코드가 한줄로  나열되어 있었는데, 이렇게 역어셈블링 과정을 살펴보면  2부터 7까지의 행으로 잘게 쪼개져있다.

 

  기계어 코드 및 역어셈블된 표현의 특징 :

      x86-64 인스트럭션들은 1~15byte 크기를 갖는다. 인스트럭션의 인코딩은 자주 사용되는 인스트럭션들과 오퍼랜드가 적은 것들이 짧은 길이를 갖게하고, 자주 사용되지 않는 경우에는    인스트럭션을 갖도록 하는 '허프만 코드' 관련이 있다.

      인스트럭션의 형식은 주어진 시작 위치에서부터 바이트들을 기계어 인스트럭션으로 유일하게 디코딩할 수 있도록 설계한다. 쉬운말로 53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3 이런식으로 길게 늘어써도, 무조건 행으로 쪼개낼  있는 매핑이 가능하다는 것이다. (예를 들면 pushq %rbx 인스트럭션만 53으로 시작하는 바이트를 가질  있다.)

      역어셈블러는 기계어 코드 파일의 바이트 순서만으로 어셈블리 코드를 결정한다. (소스 코드와 무관)

      역어셈블러는 GCC가 생성한 어셈블리 ㅋ드와는 약간 다른 명명법을 인스트럭션에 사용한다. (접미어 'q' 생략)

​         - 반대로 역어셈블러는 접미어 'q'를 call과 ret 명령어에 붙인다. (마찬가지로 생략 가능)

 

 

  3.2.3  형식에 대한 설명

 

 GCC가 생성하는 어셈블리 코드는 사람이 읽기 어렵다.

   - 필요없는 정보 포함

   - 프로그램에 대한 설명 미포함

 

'.'으로 시작하는 모든 라인은 어셈블러와 링커에 지시하기 위한 디렉티브(directive)들로, 무시해도 된다.

대부분의 디렉티브는 생략하고, 라인 번호와 주석을 포함하면 아래와 같다.

※ 어셈블리어 프로그래머가 코드를 작성하는 정석

 

 

  ※ ATT와 인텔 어셈블리 코드 형식

 

  본문에서는 어셈블리 코드를 (AT&T사의 이름을 딴) ATT형식으로 나타내며, GCC, OBJDUMP 등 여러 도구들에서도 사용하는 기본 형식이다.

function:
    pushq	%rbx
    moveq	%rdx, %rbx
    call	mult2
    movq	%rax, (%rbx)
    popq	%rbx
    ret

 

  GCC로 다음과 같은 명령어를 사용하면 function을 인텔 형식의 코드로 생성해준다.

linux$ gcc -Og -S -masm=intel function.c

 

 

인텔 어셈블리 코드

function:
    push	rbx
    move	rdx, rbx
    call	mult2
    mov		QWORD PTR [rbx], rax
    pop		rbx
    ret

 

 

ATT와 인텔 형식은 아래와 같은 차이가 있다.

      인텔 코드는 크기를 나타내는 접미어 생략

       - pushq, movq 대신 push, mov 사용

      인텔 코드는 '%' 문자를 레지스터 이름 앞에서 생략

      인텔 코드는 다른 방법을 사용해서 메모리 위치를 나타냄

       - (%rbx) 대신 QWORD PTR [rbx]

      여러 개의 오퍼랜드를 갖는 인스트럭션은 역순으로 나열함

       - 따라서, 두 형식 간에 전환하는 경우에는 매우 복잡해짐