포스트

CSAPP › 프로그램의 실행과정(Week3_Day1)

오늘은 CSAPP 3장을 읽어보았다.

컴퓨터 프로그램

컴퓨터는 프로그램을 실행 할 때, 기계어 코드를 실행한다.

기계어 : 이진수로 표현되는 저수준 언어로 하드웨어와 직접 상호작용할 수 있고, 그렇다 보니 작성하고, 이해하기 어려우며 cpu 종속적이라 이식성이 낮다. 어셈블리어 : 기계어와 대응되는 저수준 언어로 기계어보다 사람이 읽고 이해하기 쉬운 형태를 가진다.

고로 고수준 언어로 작성 된 소스코드는 컴파일러를 통해 어셈블리어로 변환 된 이후 어셈블러에서 기계어로 변환하여 오브젝트 파일을 생성하고, 링커에서 여러 오브젝트 파일과 라이브러리를 결합하여 실행가능한 프로그램을 만들고 운영체제에서 실행하게 된다.

고급언어(고수준언어)는 생산적이며, 안정적이고 다른 많은 환경에서 컴파일과 실행이 가능하다는 장점이 있다.

그럼에도 기계어 코드를 배워야 하는 이유컴파일러 최적화에 대한 이해와, *어셈블리의 *성능을 측정하여 성능을 극대화 할 수 있으며, 고급언어의 쓰레드 패키지를 통해 동시 프로그래밍 시 공유 자원이 오가는 것을 머신레벨에서 파악 할 수 있고, 정보를 변경하고 시스템 제어권을 원하는 공격들이 노리는 프로그램의 취약성에 대해 알 수 있다.

프로그램의 인코딩

이 책에서는 gcc, x86_64 기준으로 설명중.

gcc : 여러가지 언어를 지원하는 컴파일러 모음.

x86_64 : x86_32 아키텍처의 확장 버전으로 64비트 메모리 주소를 가지고, 레지스터의 확장, 호환성 등의 장점이 있다.

%r 어쩌구 : 레지스터의 이름 각각의 줄은 인스트럭션.

  • pushq : 스택에 삽입.
  • mov: 한 곳에서 다른 곳으로 복사.
  • call : procedure(function, method)를 호출
  • popq : 스택에서 꺼내.
  • ret : 이 함수를 종료하고 결과값을 반환.

컴파일 과정 :

gcc -Og -S mstore.c gcc : 컴파일러야 -Og : optimizing 해서 -S : 어셈블리 파일까지만 만들고 멈추고 mstore.c : 파일을 만드렴.

.어쩌고 있는 것들은 인스트럭션이 아님.

C 프로그램을 두 파일 p1.c p2.c에 작성 -> C 전처리기가 #include로 명시된 파일을 코드에 삽입, #define으로 선언된 매크로 정의 -> 컴파일러가 두 소스 파일의 어셈블리버전 p1.s, p2.s를 생성. -> 어셈블러어셈블리코드를 바이너리 목적코드 p1.o, p2.o로 변환. -> 링커가 두 목적파일을 라이브러리 함수를 구현한 코드와 합쳐 최종 실행파일인 p를 생성한다.

#include : 다른 파일의 내용을 현재 파일에 삽입하는 역할. #define : 매크로를 정의하고, 매크로는 특정 텍스트를 다른 텍스트로 치환한다. 목적코드 : 모든 인스트럭션이 기계어로 변환 된 상태 but 전역 변수와 함수 등의 주소가 결정 되지 않은 상태.(다른 오브젝트 파일에서도 참조 될 수 있기 때문에 링크 단계에서 결정 됨)

기계수준 프로그래밍 :

주로 어셈블리어와 기계어를 사용하는 가장 낮은 수준의 프로그래밍

  • 중요한 점 :
    • 프로그램의 포맷과 행동은 ISA에 의해 결정됨.
    • 사용되는 메모리 주소는 모두 가상주소.
    • 보여지는 프로세서 상태의 대부분은 프로그래머에게 보이지 않음.

      인스트럭션 : 특정 작업을 수행하도록 프로세서에게 지시하는 기계 명령어. ISA(Instruction Set Architecture) : 프로세서의 상태, 인스트럭션의 형식, 프로세서의 상태에 대한 각 인스트럭션들의 영향을 정의하는 아키텍쳐.

레지스터의 종류 :

레지스터 : CPU가 요청을 처리하는데 필요한 데이터를 일시적으로 저장하는 다목적 공간으로 일시적 메모리의 역할도 하며, 캐시 메모리보다도 빠름.

  • 프로그램 카운터(PC) : 다음에 실행 될 명령어의 메모리 주소를 가리키는 레지스터.
  • 정수 레시트터 파일(Integer Register File) : 64비트 크기의 데이터를 저장할 수 있는 레지스터들의 모음(주로 정수 데이터와, 메모리 주소를 저장).
  • 조건코드 레지스터(Condition Register File) : 최근에 실행 된 산술 또는 논리 명령의 결과 상태를 저장.
  • 벡터 레지스터(Vector Registers) : 벡터 연산을 지원하기 위해 하나 이상의 데이터 값을 저장(부동소수점 수치, 정수 데이터 모두 저장 가능).

기계수준 메모리 :

C언어의 경우 메모리에 할당 할 수 있는 데이터 타입을 제공 하지만 기계어는 메모리를 단순히 바이트 주소지정이 가능한 큰 배열로 봄(실제로 바이트 단위로 접근되며, 각 바이트에 고유한 메모리 주소가 할당되어, 데이터의 타입에 대한 구분은 없고, 메모리 주소에 접근하여 처리함).

  • 프로그램은 가상 주소를 사용하며, x86_64의 경우는 64비트 워드로 표현.

    가상주소 : x86_64 아키텍처에서는 64비트 가상 주소 공간을 제공함. 운영체제는 가상 주소 공간을 실제 물리 메모리 주소로 매핑하여 프로세스가 접근 할 수 있도록 함.

    • 페이지 테이블 : 가상 주소를 물리 주소로 변환하기 위한 매핑 정보를 가지며 각 프로세스마다 페이지 테이블을 유지.
      • 가상주소 해석 : 메모리에 접근하게 되면 cpu는 이를 해석하여 실제 메모리 접근의 필요성을 판단.
      • 페이지 폴트 처리 : 가상 주소가 해당하는 페이지가 메모리에 없는 경우(페이지 폴트) 이 페이지를 물리 메모리에 가져옴.
      • 가상 주소를 물리 주소로 변환 : 페이지 테이블을 사용하여 변환.
      • 물리 메모리 접근 : 메모리에 데이터를 읽거나 씀. 장점 : - 메모리 보호 : 각 프로세스는 가상 주소 공간을 가지고, 다른 프로세스의 메모리에 접근 할 수 없기 때문에, 상호 간섭을 방지하고 안정성을 높힘. - 메모리 공유 : 여러 프로세스가 동일한 물리 메모리 페이지를 공유 가능. - 스와핑 : 필요에 따라 메모리의 일부를 디스크로 스왑아웃하여 더 많은 프로세스 실행.
    • 상위 16비트는 반드시 0이여야 함. -> 주소는 2^48(64TB)만큼의 범위를 잠재적으로 지정 가능.
  • 링커:
    • 심볼 결합(Symbole Resolution) : 각 오브젝트 파일에서 사용 된 함수나 변수의 이름을 해석하고 모아서 심볼 테이블을 구성.(하나로 통합 또는 라이브러리 파일에서 참조 할 수 있도록 연결, 각 심볼의 이름과 정의된 위치를 연결).
    • 주소 변경(Address Binding) : 각 함수나 변수가 메모리에서 실제 위치를 가지도록 링크.
    • 코드와 데이터 결합(Combining Code and Data) : 각 오브젝트 파일에서 생성된 코드와 데이터 세그먼트를 하나의 실행가능 한 세그먼트로 결합(상호 의존성을 해결하여 실행파일의 올바른 작동을 준비).
    • 리소스 해결(Resource Fixing) : 프로그램 실행 도중 필요한 리소스를 링크.
    • 최적화(Optimization) : 링크 과정에서 코드의 크기를 줄이거나 최적화(코드 블록이 정렬이 맞지 않으면 NOP(No Operation)을 추가하여 메모리 접근을 최적화).

데이터의 형식

어셈블리어의 특징:

  • 1, 2, 4, 8 바이트의 integer data
  • 4, 8, 10 바이트의 floating data
  • 자료구조는 없음(이건 컴파일러가 구축하는 것)
  • 각 명령은 매우 제한적인 것을 할 수 있음(덧셈, 곱셈, 메모리 데이터 레지스터로 복사 등).

기계어 명령의 예시:

  • 일반적으로 local value 인 t는 레지스터에 저장.
  • *dest는 포인터 변수로, 이게 좌변에 있다면 내가 원하는 값(숫자 등)을 가리키는 주소에 저장하려는 것. 따라서 dest는 주소를 담고 있고 이 주소 값도 레지스터(%rbx)에 저장하고 있음.
  • mov 인스트럭션을 통해 %rax라고 불리는 레지스터에서 값을 복사해서 %rbx 레지스터가 가리키는 메모리 주소에다가 저장을 함.
  • 이 목적 코드는 3바이트 차치.

disassembling object code:

  • assembler가 소스코드를 bytes로 바꾸는 것의 반대로 수행함.
  • 바이트를 주고 어셈블리어로 바꿔줌.
  • 기존의 소스코드의 변수나 이름은 어셈블리어 수준에서 알지 못함. 레지스터나 메모리에 저장 되있겠지만 프로그램은 이를 모름.
  • 각각의 bytes는 어셈블리어로 오른쪽에 나타남.
  • push는 1, mov는 3바이트로 각 인스트럭션이 얼마를 차지하는지 알 수 있음.
  • disassembler는 소스코드를 몰랐음.어셈블리어에도 접근할 수 없었음. 오브젝트 파일의 바이트 만 보고 찾아낸거임.
  • 디버깅 프로그램들을 통해서도 disassemble가능.(gdb)

워드 : 16비트 데이터 타입(x86 아키텍쳐에서는 16비트 워드가 2바이트를 나타내며, 레지스터에 저장 할 때나 메모리에서 읽혀질 때 16비트 크기의 데이터로 처리됨.) 더블워드 : 인텔에서 16비트 기준이였으나 32비트로 추후 확장 되어 더블워드라 칭함. 쿼드워드 : 64비트

표에서 보이듯 C에서의 기본 데이터 타입으로 사용되는 x86_64에서는 int는 더블워드(32비트), 포인터와 long은 8바이트(64비트) 쿼드워드로 저장됨.

부동소수점 포맷:

부동소수점 : 컴퓨터는 실수를 표현 할 때, 2진수로 표현을 하는데, 0.3 처럼 2진수로 표현하지 못하는 소수가 발생함. -> 근차치의 값 저장. 이를 표현하는 방식 중 하나가 부동소수점.

  • 단일 정밀도(Single-precision, 4바이트) : C의 float타입에 대응.
  • 이중 정밀도(Double-precision, 8바이트) : C의 double타입에 대응.

정보 접근하기

x86_64 CPU는 64비트 값을 담기위한 16개의 범용레지스터를 가짐. 이름은 모두 %r로 시작(어디로 가야할까의 이름이라고 생각하면 됨). 이 레지스터 들은 integer와 포인터(주소값)을 담을 수 있음. 인스트럭션들은 16개 레지스터의 낮은 순서(각 레지스터의 바이트, 워드 , 더블워드, 쿼드워드씩을 의미)의 바이트에 저장된 서로 다른 사이즈의 데이터를 사용 가능.(바이트 -> 바이트, 16비트 -> 2바이트, 32비트 -> 4바이트, 64비트 -> 레지스터 전체)

%r은 64비트를 담을 수 있다는 것, %e는 32비트를 담을 수 있다는 것.

long 자료형이면 %r레지스터를 사용하는 것, 나머지 32비트 자료형은 %e레지스트리 사용.

예외적으로 %rsp는 stack pointer로 내맘대로 할당해서 쓰는 곳이 아님.

데이터 옮기기 :

오퍼랜드 식별자

오퍼랜드(Operand) : 명령어가 실행될 때 필요한 입력(소스값)이나 출력 값(저장될 위치).

종류 :

  • 즉시수치(Immediate) : 명령어가 직접적으로 데이터 값을 포함. (값이 몇진법인지는 어셈블러가 알아서 결정) ex) add eax, 10 일때 10이 즉시 수치 오퍼랜드.
  • 레지스터(register) : 명령어가 직접적으로 레지스터에 접근하여 연산을 수행. 레지스터 집합을 배열과 레지스터 식별자를 인덱스로 사용하는 형태. ex) add eax, ebx에서 eax, ebx 모두 레지스터 오퍼랜드이며, ebx의 값이 eax에 더해짐.
  • 메모리참조(memory) : 명령어가 메모리의 특정 주소에 접근하여 데이터를 읽고 씀. 메모리는 거대한 바이트의 배열로 생각 -> $M_{b}[Addr]$과 같이 표시하여 메모리 주소 Addr로 부터 저장 된 b바이트를 참조. 일반적으로 아래첨자는 생략. ex) mov[ecx], edx에서 [ecx]는 메모리오퍼랜드로 edx값이 ecx가 가리키는 메모리 위치에 저장.

대표적인 형태 : $Imm(r_{b}, r_{i},s)$로 각각

  • Immediate($Imm$) : 주로 상수 offset을 나타낸다.(0이라면 메모리 주소계산에 영향 x), 표기한 주소에 저장이 아니라 이동해서 저장하게 함.
  • Base register($r_{b}$) : 메모리 주소의 기본으로 사용 될 레지스터.
  • Index register($r_{i}$) : 메모리 주소를 인덱스로 조정하는데 사용 될 레지스터.
  • Scale factor($s$) : 인덱스 레지스터의 값에 곱해질 상수. 주로 $2^x$의 형태로 사용. 내가 넣을 데이터의 크기에 따라 조절 하는 것. ex) int일 경우 4, long일 경우 8.

movq source destination

imm -> 레지스터 / 메모리 레지스터 -> 레지스터 / 메모리 메모리 -> 레지스터

상수를 destination 으로 설정 할 수 없고, 메모리에서 메모리로 복사를 할때는 메모리 -> 레지스터, 레지스터 -> 메모리로 2번의 인스트럭션을 거쳐야 함(하드웨어 디자이너의 편의를 위해).

source 인지 destination인지 표기법:

레지스터이름에 괄호를 친 것 : 이 레지스터안에는 주소가 들어있고, 그 주소로 특정 메모리 위치에 접근하라.

D는 앞의 숫자만큰 offset을 주라는 얘기

%rdi와 %rsi에 xp와 yp의 포인터를 가지고, 포인터들은 메모리의 주소를 가지고 있다. 이건 swap 함수가 실행되기 전에 콜한 부분에서 이미 정해진 것.

메모리에서 읽어오기 : %rdi 의 주소를 활용하여 그 주소로부터 값을 복사하고 %rax에다가 저장하라. moveq의 q는 quadword 8바이트를 뜻함.

%rsi의 주소(0x100)을 활용하여 그 주소로부터 값(456)을 복사하고 %rdx에다가 저장하라. 메모리에 저장하기: %rdx 에 저장된 값(456)을 %rdi(0x120) 의 메모리에다가 복사.

%rax에 저장된 값(123) 을 %rsi(0x100)의 메모리에다가 복사.

base register$R_{b}$(시작주소), index register$R_{i}$(변위 참조 주소),scale(1, 2, 4, 8) 주소에 상수 곱하기

이의 유효주소는 base_address + (index_register * scale_factor) + immediate_value .

실제 주소계산 예시

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

  • 스택

    • 프로시저 호출(함수 호출을 처리하는데 중요한 역할)
    • push 연산으로 스택에 데이터 추가, pop 연산으로 읽음.
    • 읽어오는 값은 가장 최근에 추가 된 값, 스택에 계속 남아있음!
    • 원소가 나가고 들어오는 부분 = top
    • 스택은 아래로 성장하기 때문에(스택의 맨 위가 젤 아래있음) top원소가 가장 낮은 주소를 가짐.
    • 스택 포인터 %rsp(%esp)는 top의 주소를 저장.
    • pop 후 다시 push 하게 되면, 남아있던 데이터가 덮어씌워짐.
  • pushq <추가할 소스="" 데이터="">

    • 데이터를 스택에 추가
    • 스택 포인터의 주소가 감소
  • popq <추출을 위한="" 데이터="" 목적지="">

    • 스택의 top 데이터를 읽어서.
    • 데이터를 목적지에 저장.
    • 스택 포인터의 주소가 증가.

lea(Load Effective Address)

메모리를 참조 하지 않고 주소를 계산하거나, 산술 계산을 할 때 사용.

3x를 구하고, salq는 왼쪽으로 2비트 이동($x*2^n$이 때 n=2)은 4배와 같기 때문에 12x가 됨.

얘네도 똑같이 2가지 요소 src, dest를 받지만 x += y가 x = x + y 인 것 처럼 dest도 src임. 또한 src가 먼저 오고 dest가 나중에 있다는 순서 주의 할 것!(보통 a = a + b, 얘는 b, a로 주잖아.)

데이터 이동 인스트럭션

논리적, 산술적 이동에 주의할 것.

Signed Integer (서명 있는 정수): 정수는 양수와 음수 모두를 포함. 가장 왼쪽 비트(부호 비트)는 정수의 부호. 0은 양수, 1은 음수. ex) int, short, long 등. Unsigned Integer (서명 없는 정수): 정수는 양수만을 포함. 모든 비트는 정수 값을 나타내는 데 사용. ex) unsigned int, unsigned short, unsigned long 등.

논리적 이동 (SHL, SHR): 빈자리를 0으로 채움. 서명 있는 정수와 서명 없는 정수 모두 동일하게 취급. 산술적 이동 (SAR): 오른쪽 이동은 왼쪽의 빈자리를 부호비트(+이면 0, - 이면 1)로 채움. 서명 있는 정수에 대해서만 의미가 있으며, 부호 비트를 유지.

One Operand Instructions :

출처 : csapp저자 강의

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.