크래프톤 정글 WIL_Week03

pigpgw·2024년 8월 18일

크래프톤 정글

목록 보기
5/13
post-thumbnail

들어가며

3주차는 알고리즘 마지막 주차이자 c언어로 자료구조를 다루기전에 준비하는 주차이다.

ISA, X86과 ARM, x84? x64?

운영체제(Operating System)와 커널

운영체재 메모리 할당 방식

커널의 역할과 64비트 메모리 사용 (64비트 시스템에서)

컴퓨터 메모리 시스템의 이해

메모리 주소는 왜 16진수로 표현할까?

컴퓨터 프로그램은 무엇으로 이루어져 있나?

  • 컴퓨터가 실행하는 것은 기계어.
    • 기계어 : 컴퓨터가 직접 이해하고 실행할 수 있는 가장 기초적인 프로그래밍 언어. 이 언어는 컴퓨터의 중앙 처리 장치(CPU)가 직접 인식하고 처리할 수 있는 이진 코드, 즉 0과 1로 구성된 명령어 집합으로 이루어짐
    • 어셈블리어 :기계어가 사용하는 숫자의 의미를 프로그래머가 외우는 것은 어려운 일 이 숫자의 의미를 사람이 좀 더 이해하기 쉬운 단어로만 변경한 것
  • 기계어는  컴파일러->어셈블러->링커 의 과정을 거쳐 만들어진다.
    • 컴파일 : 인간이 이해할 수 있는 언어로 작성된 소스 코드를 CPU가 이해할 수 있는 언어로 변환 하는 작업을 말함
    • 어셈블러 : 저급 언어로 작성된 코드는 어셈블러라는 프로그램에 의해 CPU의 ISA 체계에 맞게 기계어로 이뤄진 오브젝트 모듈을 여러 개 만들게 된다. 그것들을 적절히 합쳐서 하나의 실행 가능한 파일로 만드는 프로그램이 링커이다.
  • 요즘 컴파일러들은 숙련된 어셈블리 프로그래머들이 짜는 것과 비슷한 어셈블리 코드를 만들어낸다.

고급언어의 유용함

  • C언어로 프로그램을 짜면 gcc로 컴파일만 해주면 코드를 재활용 할 수 있다.
    • 하지만 어셈블리로 코딩을 하면 CPU 종류마다 코드를 다 다르게 짜야 한다

하지만 어셈블리를 배워야 하는 이유

  1. 컴파일러 최적화에 대해 이해할 수 있음.

    • 이유: 어셈블리를 이해하면 고급 언어 코드가 어떻게 기계어로 변환되는지 알 수 있어 컴파일러 최적화 과정에 대해 깊이 이해할 수 있기때문에 공부해야한다.
    // C 코드
    int sum(int a, int b) {
        return a + b;
    }
    // 어셈블리 코드 (x86_64)
    sum:
        mov eax, edi
        add eax, esi
        ret
    
  2. 반복된 컴파일 과정 속에서 나오는 어셈블리의 성능을 측정해 성능을 극대화할 수 있다.

    • 이유: 어셈블리 코드를 통해 특정 코드가 실행될 때 실제로 어떤 명령어들이 사용되는지 알 수 있고 이를 통해 성능 병목 지점을 식별하고 최적화할 수 있기에 공부해야한다.
    // C 코드
    for (int i = 0; i < 1000; i++) {
        array[i] = i * 2;
    }
    // ; 어셈블리 코드 (x86_64)
    loop:
        mov eax, ecx
        shl eax, 1
        mov [rdi + rcx*4], eax
        inc ecx
        cmp ecx, 1000
        jl loop
    

    i * 2가 어셈블리에서 shl eax, 1로 최적화되어있다. 어셈블리 코드를 보면, 시프트 연산이 곱셈보다 더 빠르다는 것을 알 수 있는데 이를 통해 성능을 극대화할 수 있다.

  3. 고급언어가 추상화하는 프로그램의 런타임 행동을 이해할 수 있다.

    • 이유: 어셈블리 언어는 고급 언어가 추상화하는 프로그램의 런타임 행동을 명확하게 보여준다. 이를 통해 메모리 관리, 스택 프레임, 레지스터 사용 등을 이해할 수 있다.
    // C 코드
    int factorial(int n) {
        if (n <= 1) return 1;
        else return n * factorial(n - 1);
    }
    // ; 어셈블리 코드 (x86_64)
    factorial:
        cmp edi, 1          ; (1) n <= 1인지 비교
        jle .L2             ; (2) 조건이 참이면 .L2로 점프
        sub edi, 1          ; (3) n = n - 1
        call factorial      ; (4) 재귀 호출
        imul eax, edi       ; (5) n * factorial(n - 1)
        add edi, 1          ; (6) n을 원래 값으로 복원
        ret                 ; (7) 반환
    .L2:
        mov eax, 1          ; (8) n <= 1인 경우 1을 반환
        ret                 ; (9) 반환

    레지스터 사용 이해

    • EDI: 함수 인자 n을 저장하는 레지스터
    • EAX: 함수의 반환 값을 저장하는 레지스터

고급 언어가 추상화한 프로그램의 런타임 행동 이해

  1. 재귀 호출의 동작:

    • 재귀 호출 시 각 호출의 상태가 스택에 저장되며, 함수 호출이 끝나면 스택 프레임이 제거된다.
    • 스택 프레임을 통해 각 호출의 독립적인 변수 상태가 유지된다.
  2. 메모리 관리:

    • 레지스터를 통해 변수 n의 값을 저장하고, 재귀 호출을 위해 값을 감소시킨 후 다시 원래 값으로 복원하는 과정을 통해 메모리 관리의 기본적인 개념을 이해할 수 있다
    • 스택을 사용하여 함수 호출 시 필요한 데이터를 저장하고 관리하는 방법을 배울 수 있다.
  3. 레지스터 사용:

    • 함수 인자와 반환 값을 레지스터를 통해 관리하는 방법을 이해할 수 있다.
    • 레지스터를 사용하여 연산을 수행하고, 결과를 반환하는 과정을 이해할 수 있다.
  4. 고급 언어의 쓰레드 패키지를 통해 동시 프로그래밍을 할 때 공유 자원이 오가는 것을 머신 레벨에서 파악 가능.

    • 이유: 어셈블리를 통해 스레드 간의 자원 접근과 동기화 메커니즘을 머신 레벨에서 파악할 수 있습니다. 이는 동시 프로그래밍의 복잡성을 이해하고 문제를 디버그하는 데 도움이 됩니다.
    // C 코드 (Mutex를 사용한 예시)
    pthread_mutex_t lock;
    
    void *increment(void *arg) {
        pthread_mutex_lock(&lock);
        count++;
        pthread_mutex_unlock(&lock);
        return NULL;
    }
    //; 어셈블리 코드 (x86_64)
    increment:
        push rbp
        mov rbp, rsp
        mov rdi, lock
        call pthread_mutex_lock
        mov rax, count
        add rax, 1
        mov count, rax
        mov rdi, lock
        call pthread_mutex_unlock
        pop rbp
        ret
    
  5. 프로그램의 취약점을 방어할 수 있다.

// C 코드 (취약한 코드)
void vulnerable_function(char *str) {
    char buffer[10];
    strcpy(buffer, str);
}
; 어셈블리 코드 (x86_64)
vulnerable_function:
    push rbp
    mov rbp, rsp
    sub rsp, 16
    mov rdi, buffer
    call strcpy
    leave
    ret

이 예제에서 strcpy를 사용한 버퍼 오버플로우 취약점을 볼 수 있습니다. 어셈블리 코드를 보면, buffer가 스택에 할당되고 strcpy가 호출되며 버퍼 크기를 넘는 데이터가 쓰여질 수 있음을 알 수 있습니다. 이를 통해 스택 보호 기법(예: 스택 캔어리)을 이해하고 적용할 수 있습니다.

결론

어셈블리 언어를 배우면 컴파일러 최적화, 성능 측정 및 최적화, 프로그램의 런타임 행동, 동시 프로그래밍의 동기화 문제, 그리고 프로그램 취약점의 방어 등에 대해 깊이 이해할 수 있고 이러한 지식은 고급 언어 프로그래밍을 더욱 효과적이고 안전하게 만드는 데 큰 도움이 된다.

gcc -Og -S 파일명.s

  1. gcc: GCC 컴파일러를 실행
  2. Og: 최적화 옵션으로, 디버깅에 최적화된 수준의 최적화를 수행
    • GCC에는 여러 최적화 레벨이 있습니다. O0(최적화 없음), O1, O2, O3(최고 수준 최적화), 그리고 디버깅을 염두에 둔 Og가 있다.
    • Og는 성능 향상과 함께 디버깅 정보를 유지하여 디버깅이 용이하도록 한다.
  3. S: 소스 코드를 어셈블리 코드로 변환하고, 어셈블리 코드를 파일로 출력
    • S 옵션은 컴파일러가 소스 코드를 어셈블리 코드로 변환한 후, 이를 실행 파일로 링크하지 않고 어셈블리 코드 상태로 남겨둠
    • 이는 소스 코드가 어떻게 어셈블리 코드로 변환되는지 확인하고자 할 때 유용
  4. 파일명.s: 출력될 어셈블리 코드의 파일명
    • 파일명.s 파일은 주어진 C 소스 코드의 어셈블리 버전을 담고 있게 됨

따라서, gcc -Og -S asdad.s 명령어를 실행하면, GCC는 다음과 같은 작업을 수행

  1. 주어진 소스 코드를 읽어들임
  2. Og 옵션에 따라 디버깅을 위한 적절한 최적화를 수행
  3. 소스 코드를 어셈블리 코드로 변환
  4. 변환된 어셈블리 코드를 파일명.s 파일로 저장

3.2 Program Encodings

책에서는 gccx86_64 기준으로 설명하고 있음. gcc 버전에 따라 어셈코드가 달라 보일 수 있음

컴파일 과정 요약

소스코드를 기계가 이해할 수 있는 언어로 변환하는 과정 : 전처리 → 컴파일 → 링킹

  1. 전처리 단계 : 프로그램의 구조를 정의하고, 컴파일러에게 필요한 정보를 제공하는 중요한 단계
    • 소스 코드 파일에서 #include, #define과 같은 전처리 지시어를 처리 (매크로의 확장, 조건부 컴파일, 파일 포함 등의 작업이 수행)
      • 전처리 지시어는 컴파일러에게 소스 코드를 어떻게 처리할지 알려주는 역할을 하기 때문
        • ex : #include 지시어는 다른 파일의 내용을 현재 파일에 포함시키는 역할을 함
  2. 컴파일 단계
    1. 전처리된 소스 코드를 오브젝트 코드로 변환하는 과정
      1. 소스 코드의 문법을 분석하고 중간 코드를 생성한 뒤, 최적화 과정(프로그램의 성능과 효용성에 직접적인 영향을 미치는 최적화 작업 ⇒ 프로그램의 실행 속도와 메모리 사용량을 개선하는 데 중요)을 거쳐 오브젝트 코드를 생성
        1. 오브젝트 코드는 기계어 코드와 유사하지만, 완전한 실행 파일은 아니기 떄문
        2. 오브젝트 코드는 링킹 단계에서 다른 오브젝트 파일들과 결합되어 실행 파일을 형성
  3. 링킹 단계
    1. 여러 오브젝트 파일을 하나의 실행 파일로 결합하는 과정
      1. 외부 라이브러리와의 연결, 전역 변수와 함수의 주소 결정등이 수행됨
      2. 프로그램의 실행과 직접적으로 관련되어 있음, 이 단계를 통해 최종적인 실행 파일이 생성
        1. 링커가 오브젝트 코드들을 실제 코드가 구현된 라이브러리의 주소와 일치시켜서 마침내 실행 가능한 프로그램을 만든다.
          1. 링커란 기계어를 운영체제의 커널과 연결해, 운영체제가 실행할 수 있는 파일로 만들어줌

머신레벨 프로그래밍에서 중요한 것

1. 프로그램의 포맷과 행동은 ISA에 의해 결정됨

1.3 어셈블리어

  • 기계어: 마이크로프로세서가 인식하여 실행할 수 있는 명령어들로 구성된 바이너리 코드입니다. 사람이 이해하고 작성하기 어렵다.
  • 어셈블리어: 기계어 명령어를 사람이 이해하기 쉽게 문자화한 것, 각 기계어 명령어에 대응하는 기호나 단어를 사용한다.. 예를 들어, MOV는 데이터를 이동시키는 명령어를 의미한다.

2. 사용되는 메모리 주소는 모두 Virtual Addresses이다

  • 가상 주소(Virtual Addresses): 프로그램이 실행될 때 사용하는 메모리 주소는 실제 물리적 메모리 주소가 아니라 가상 주소이다. 가상 주소는 운영체제가 관리하며, 프로그램이 직접 물리적 메모리에 접근하는 것을 방지하고 메모리 보호, 주소 공간 분리, 효율적인 메모리 사용을 가능하게 한다.
  • 메모리 매핑: 운영체제는 가상 주소를 물리적 주소로 변환하는 역할을 한다. 이를 메모리 매핑 또는 주소 변환이라고 하고. 주로 MMU(Memory Management Unit)가 이 작업을 수행한다.

3. 보여지는 프로세서 상태의 대부분은 C 프로그래머에게 감춰져 있음

  • 추상화된 프로그래밍: 고급 프로그래밍 언어(C, Java 등)는 프로세서의 상태나 하드웨어 자원에 대한 직접적인 접근을 추상화한다. 이는 프로그래머가 시스템의 복잡한 세부 사항을 신경 쓰지 않고 코딩할 수 있게 해준다.
  • 프로세서 상태: 레지스터, 프로그램 카운터, 플래그, 메모리 상태 등 프로세서가 현재 작업을 수행하는 데 사용하는 다양한 상태 정보들이 있습니다. 이런 상태들은 주로 하드웨어 수준에서 관리되며, 고급 언어에서는 직접 다루지 않는다.
  • 캡슐화: C와 같은 고급 언어는 컴파일러와 운영체제를 통해 하드웨어 세부 사항을 캡슐화한다. 예를 들어, 변수 선언과 같은 단순한 작업이 실제로는 메모리 할당, 주소 계산 등 복잡한 작업을 포함한다.

이러한 개념들은 머신 레벨 프로그래밍을 이해하고 하드웨어와 소프트웨어 간의 상호 작용을 깊이 있게 이해하는 데 매우 중요한데 ISA는 하드웨어와 소프트웨어 간의 다리 역할을 하며, 가상 주소는 메모리 관리를 효율적이고 안전하게 만들어 준다. 고급 언어의 추상화는 프로그래머가 하드웨어의 복잡한 세부 사항을 신경 쓰지 않고 효율적으로 코딩할 수 있게 한다.

레지스터의 종류

  • Program Counter (PC, %rip(x86_64): 다음 명령이 실행될 메모리 주소
  • 정수 register file : 64비트의 값을 담는 16개의 이름이 있는 위치, C포인터와 대응하는 주소나 정수 값을 저장
  • Condition code register 최근에 실행된 산술 혹은 논리 명령의 상태 정보를 담고 있음
  • 벡터 레지스터: 하나 이상의 정수나 부동소수점(float) 값을 저장

머신 레벨 메모리

  • C언어는 메모리를 점유할 수 있는 여러 데이터 타입들을 제공하지만, 기계어는 메모리를 거대한 주소로 접근가능한 바이트들의 배열로 본다.
    • C언어와 같은 고급 언어에서는 우리가 다양한 데이터 타입(int, char, float 등)을 사용하여 메모리에 접근하지만, 컴퓨터 내부적으로는 메모리를 단순히 큰 주소 공간으로 바리본다
  • 프로그램은 가상 주소를 사용하며 x86_64의 가상 주소의 경우 64비트 워드로 표현됨.
    • 가상 주소 공간 : 프로세스가 사용할 수 있는 메모리 주소의 범위
      • 운영체제는 각 프로그램에 대해 고유한 가상 주소 공간을 제공하여 로그램들이 서로의 메모리에 간섭하지 않도록 함
  • 상위 16비트는 반드시 0이여야 함. 그러므로 주소는 2의 48 (64TB)만큼의 범위를 잠재적으로 지정 가능.
  • 운영체제는 이러한 가상 주소 공간을 관리해서 가상 주소를 실제 프로세서 메모리의 물리적인 값으로 바꾼다.

링커의 역할

  • 링커는 코드의 위치를 다른 주소 범위로 옮긴다. 즉, 실제로 호출될 함수의 주소와 이름으로 callq 명령어의 인자를 바꿈.
  • 또한 함수 호출을 실행가능한 코드의 위치로 대체함.
  • 코드 블록이 16바이트가 되지 않으면 return(req) 다음에 nop라는 명령어가 삽입되지만 아무일도 하지 않고 단지 메모리 시스템의 성능을 위한 것이다.

3.3. Data Formats

부동 소수점 포맷

부동 : 한자로 떠다니며 정해진 위치가 없음

고정 소수점 : 숫자를 정수 부분과 소수 부분으로 나누어 각각 고정된 위치에 저장

  • 소수 부분은 정확히 표현 불가능
  • 표현 가능한 범위가 매우 적음

부동 소수점 : 큰 범위의 값을 표현하기 위해서

스크린샷 2024-07-19 오후 3.13.23.png

가수와 지수로 구분하여 표시

  • 부호는 1비트 , 가수는 23비트, 지수는 8비트

스크린샷 2024-07-19 오후 3.18.41.png

부동 소수점, 고정 소수점

  • 큰 범위의 값을 표현하기 위해서

부동소수점 수는 두가지의 포맷으로 나뉜다.

  • Single-precision (4 바이트) 값, C의 float 와 대응함.
  • Double-precision (8 바이트) 값, C의 double 과 대응함

또한 x86계열의 프로세서들은 역사적으로 특별한 80비트 (10바이트) all floating-point 명령을 이용했다. (호환되지 않을 수 있음)

3.4 Accessing Information (Figure 3.2)

  • x86_64 CPU는 64비트 값을 담기 위한 16개의 general-purpose-registers 를 가지고 있다. 레지스터들의 이름은 %r 로 시작함.
  • 명령어들은 16개 레지스터의 낮은 순서의 바이트에 저장된 서로 다른 사이즈의 데이터를 운용할 수 있다. (16비트 명령어는 마지막 2바이트, 32비트는 마지막 4바이트, 64비트는 전체 레지스터)
  • Stack pointer 인 %pstp 는 런타임 스택에서 마지막 자리를 가리키는 데 쓰인다. 몇몇 명령어들은 이 레지스터에 I/O를 수행함.

명령어 구성

하나의 명령어는 보통 “연산자 코드”, “모드”, “피연산자 지시자”로 구성됨

  • 연산자 코드 : 수행할 연산자의 종류를 나타내며 보통 operation code라고 불림
  • 모드 : 피연산자 지시자에 의한 피연산자의 유효 주소를 결정하는 모드를 결정
  • 피연산자 자시자 : 피연산자에 대한 정보를 표시하는 부분으로써 메인 메모리의 주소, 레지스터 번호, 또는 사용할 데이터 등을 표시

3.4.1 Operand Specifiers

  • 오퍼랜드는 명령어의 일부로 직접적으로 포함된 상수 값
    • 피 연산자를 의미하며, 피연산자는 명령의 일부로서 연산자에 의해 처리되는 데이터를 말함
      • 숫자 두 개를 더하는 경우 두 숫자는 피연산자이고 “+”는 연산자입니다.
  • 연산에 필요한 데이터가 명령어 자체에 직접 적혀있다.

명령어의 피연산자는 아래 세가지 타입으로 분류된다.

오퍼랜드의 세 가지 타입

  1. Immediate (즉시 값)
    • 설명: 명령어에 직접 포함된 상수 값입니다. 예를 들어, mov $5, %eax에서 $5는 즉시 값으로, 레지스터 eax에 5를 저장합니다.
    • 표기법: ATT 포맷에서는 $ 기호 뒤에 정수를 붙여 사용합니다. 예: $-577 또는 $0x1F.
  2. Register (레지스터)
    • 설명: CPU의 내부 레지스터를 참조하는 오퍼랜드입니다. 레지스터는 데이터의 임시 저장소 역할을 합니다.
    • 예시: add %eax, %ebx에서 %eax%ebx는 레지스터입니다. 이 명령어는 eax의 값을 ebx에 더합니다.
  3. Memory (메모리)
    • 설명: 특정 메모리 주소에 저장된 값을 참조합니다. 메모리 주소는 계산된 주소를 통해 접근합니다.
    • 예시: mov 0x10(%ebx), %eax에서 0x10(%ebx)는 메모리 주소를 참조합니다. 이 명령어는 ebx 레지스터의 값에 16을 더한 주소의 값을 eax 레지스터에 저장합니다.

요약

  • Immediate: 명령어에 직접 포함된 상수 값. 예: $5.
  • Register: CPU의 레지스터. 예: %eax.
  • Memory: 메모리 주소의 값. 예: 0x10(%ebx).

이렇게 각각의 오퍼랜드 타입을 이해하면 어셈블리 언어에서 데이터를 어떻게 처리하는지 쉽게 파악할 수 있다.

Operand forms (Figure 3.3)

  • 가장 일반적인 형태는 Imm(rb,ri,s) 이다.
  • 위 형태는 Imm+R[rb]+R[ri]*s 로 계산됨. 이런 형태는 보통 배열의 원소에 접근할 떄 볼 수 있음.
  • 그림의 나머지 형태는 위 식에서 몇 가지가 빠진 것임.

    Operand Forms (오퍼랜드 형태) 쉽게 설명하기

    어셈블리 언어에서 오퍼랜드의 형태는 메모리 주소를 계산하거나 데이터를 참조하는 방법을 정의합니다. Figure 3.3에서 보이는 다양한 형태들은 이러한 주소 계산 방식의 예시입니다. 가장 일반적인 형태는 다음과 같습니다:

    1. Imm(rb, ri, s) 형태

    • 설명: Imm(rb, ri, s)는 메모리 주소를 계산할 때 사용하는 일반적인 형태입니다.

    • 계산 방식: Imm + R[rb] + R[ri] * s

      • Imm: 즉시 값 (Immediate value)
      • R[rb]: 레지스터 rb의 값
      • R[ri] * s: 레지스터 ri의 값에 s를 곱한 값
    • 용도: 이 형태는 주로 배열의 원소에 접근할 때 사용됩니다.

      예시로 이해하기

    1. 배열 접근 예시:
      - 배열: arr[5]라는 배열이 있다고 가정해봅시다.
      - 주소 계산: 배열의 원소에 접근하기 위해 메모리 주소를 계산해야 합니다.
      - 계산: Imm + R[rb] + R[ri] * s 형태로 주소를 계산합니다.
      - Imm: 배열의 시작 주소 (예: 0x1000)
      - R[rb]: 배열의 시작 주소 (예: 0x1000)
      - R[ri] * s: 배열의 인덱스와 원소 크기 (예: 5 * 4 바이트 = 20 바이트)

      **주소 계산 예시**: 만약 `Imm`가 0x1000이고, `R[rb]`가 0이고, `R[ri]`가 5이며 `s`가 4라면:
      
      - 계산: `0x1000 + (5 * 4) = 0x1000 + 20 = 0x1014`
      - 결과: 주소 0x1014에 위치한 데이터에 접근합니다.

      2. 다른 형태

      그림의 나머지 형태들은 위의 기본 형태에서 일부가 빠진 것이며, 다양한 주소 계산 방식을 제공합니다

      요약

    • Imm(rb, ri, s) 형태는 주소를 계산할 때 가장 일반적으로 사용되는 형태입니다.

    • 주소 계산: Imm (즉시 값) + R[rb] (기본 주소) + R[ri] * s (인덱스와 스케일)

    • 용도: 배열의 원소에 접근하거나, 메모리 주소를 동적으로 계산할 때 사용됩니다.

      이해를 돕기 위해 간단한 예시와 설명을 통해 오퍼랜드 형태와 주소 계산 방식을 정리했습니다.

3.4.2 Data Movement Instructions

데이터 이동 명령어: 데이터를 메모리와 레지스터 간에 복사하는 데 사용됨, 각 명령어는 이동할 데이터의 크기에 라 다름

데이터 이동 명령의 source operand들은 값들이 메모리 혹은 레지스터에 담긴 Imm이여야 함. Destination operand로는 레지스터나 메모리 주소를 지정함.

  • movb Move byte
  • movw Move word
  • movl Move double word
  • movq Move quad word
  • x86_64에서는 Move 명령어 소스와 목적지 모두 메모리를 가리킬 수 없다. 즉 한 메모리 위치에서 다른 메모리 위치로 직접 데이터를 복사할 수 없으며 두 개의 명령어를 사용해야 함
    • source 에는 상수, 레지스터 저장 값, 메모리 저장 값
    • Destination에는 레지스터 , 메모리 주소
      • 단 Source와 Destination가 동시에 메모리를 가리킬 수 없다.
  • 대부분의 경우 mov 는 destination operand가 가리키는 특정한 레지스터 바이트 혹은 메모리 위치만을 업데이트한다.
  • 예외는 movl(32비트)의 목적지가 레지스터 일때, 레지스터 값의 높은 순위의 4바이트가 0으로 설정된다.

movz , movs

movz 계열 명령어들은 남은 바이트를 0으로 채운다. 그러나 movs 명령어들은 sign extension된 값들을 채운다. 그리고 source operand의 MSB(Most Significant Bit)를 복제한다.

3.4.3 Data Movement Example

  • C code
long exchange(long *xp, long y) {
	long x = *xp;
    *xp = y;
    return x;
}
  • Assembly code
exchange:
	movq (%rdi), (%rax)
    	movq %rsi, (%rdi)
    	ret

위 코드가 유용하지 않은 이유

  • C 언어에서 포인터는 단지 주소이다.
  • 포인터를 간접참조하는 건 주소를 레지스터로 복사하는 것이고 레지스터를 메모리의 레퍼런스로 쓰는 것이다.
  • 두번째로 지역변수는 보통 메모리 보다 레지스터에 저장되는 것이 빠른데 왜냐하면 레지스터에 접근하는 것이 더 빠르기 때문이다.

최적화된 코드

_exchange:
LFB0:
       	pushq  	%rbp
LCFI0:
       	movq   	%rsp, %rbp
LCFI1:
       	movq   	%rdi, -24(%rbp)
       	movq   	%rsi, -32(%rbp)
       	movq   	-24(%rbp), %rax
       	movq   	(%rax), %rax
       	movq   	%rax, -8(%rbp)
       	movq   	-24(%rbp), %rax
       	movq   	-32(%rbp), %rdx
       	movq   	%rdx, (%rax)
       	movq   	-8(%rbp), %rax
       	popq   	%rbp
LCFI2:
       	ret

3.4.4 Pushing and Popping Stack Data

  • x86_64 에서는 프로그램 스택이 메모리의 특정 영역을 차지한다. 그리고 스택애서 top element 가 가장 낮은 주소를 차지 하고 있다. %rsp 스택 포인터가 top 스택 원소의 주소를 가지고 있다.
  • 스택에 quad word(8바이트) 를 push 할때는 먼저 스택 포인터를 8만큼 감소시키고 값을 새로운 top 주소에 쓰는 것이다.
  • 스택에서 값을 꺼낼때 (pop)할 때는 원래대로 top 포인터를 8만큼 증가시키면 우리가 꺼낸 값 0x123이 여전히 메모리를 가리키고 있다. (다른 푸시 연산이 있기 전까지는) 그러나 stack의 탑은 언제나 %rsp 가 가리키는 주소로 유지된다.

3.5. Arithmetic and Logical Operations

3.5.1 Load Effective Address

  • leaq 명렁어는 movq 명령어의 변종이다. 메모리로부터 데이터를 읽어 레지스터로 보내지만 메모리를 참조하지 않는다.
  • 첫 피연산자가 메모리 레퍼런스지만 지정된 위치를 읽는 대신 이 명령은 destination에 효과적인 주소를 복사한다.
  • operand에 &이 달려있는 것처럼 이 명령은 나중에 오는 메모리 레퍼런스를 위한 포인터를 만들떄도 쓰인다.
  • 다른 산술 연산을 묘사할때도 쓰일 수 있다. 예를 들어 %rdx 가 x라는 값을 가지고 있을 때 leaq 7(%rdx, %rdx,4)를 하면 5x+7이 된다.
  • Destination operand는 반드시 레지스터여야 한다.

3.5.2 Unary and Binary operation

Unary

  • 단항 연산은 operand가 메모리나 레지스터 위치이다.
  • 예를 들어 incq (%rsp)는 스택의 8byte(quad word) top 원소가 증가하게 된다. 이 문법은 C의 ++나 -- 와 같다.

Binary

  • 다음 이항 연산 그룹은 C의 x-=y 같은 구문을 연상시킨다. 예를 들어 subq %rax , %rdx 같은 명령은 %rdx를 %rax의 값으로 감소시킨다.

3.5.3 Shift Operations

  • Shift 명령어들은 이동할 양을 Imm이나 단일 바이트 레지스터인 %cl로 정할 수 있다. (이 명령어들은 오직 한 레지스터만을 피연산자로 쓴다는 점에서 이상하다)
  • x86_64에서는 w비트의 길이를 가진 데이터 값으로 동작하는 쉬프트 명령어는 쉬프트의 양을 레지스터 %cl의 낮은 순서 m비트로부터 결정하다. (2^m =w) 높은 순서의 비트들은 무시된다.
  • 예를 들어 %cl 이 0xFF라면 salb 는 7만큼 쉬프트 (w가 8이므로 m 은 3이고 0xFF의 낮은 3비트의 값은 7이기 때문이다)

3.6.1. Condition codes

  • CF: Carry flag. 가장 최근의 연산에서 MSB에 자리 올림이 발생한 상황.
  • 가장 최근의 연산이 부호가 없는 연산의 오버플로우를 감지하기 위해 사용된다.
  • ZF: Zero flag: 가장 최근 연산이 0
  • SF: Sign flag. 가장 최근 연산이 음수
  • OF: Overflow flag: 가장 최근 연산이 2의 보수 오버플로우를 일으킴
  • Example
CF (unsigned) t < (unsigned) a Unsigned overflow
ZF (t==0)  0
SF (t<0) Negative
OF (a < 0 == b < 0) && (t < 0 != a < 0)  Signed Overflow                                                                                            

키워드

  • x86 , 80x86 : 인텔이 개발한 마이크로프로세서 계열을 부르는 말이자, 이들과 호환되는 프로세스 들에서 사용한 명령어 집합 구조들을 통칭
  • 명령어 집합 (구조) : 마이크로 프로세서가 인식해서 기능을 이해하고 실행할 수 있는 기계어 명렁
  • 마이크로프로세서 (CPU) : 컴퓨터의 중앙 처리 장치(CPU)를 말함
    • 기계어 코드를 실행하기 위해 실행 과정을 단계별로 나누어 처리를 위한 마이크로 코드를 작성하고, 이. 마이크로 코드에 의해 단계적으로 처리하는 논리회로를 말함
      • 프로세서 또는 처리 장치는 컴퓨팅 . 및 컴퓨터 과학에서 외부 데이터 소스에서 작업을 수행하는 전기 부품 회로
  • 프로그램 : 디스크에 저장된 정적인 코드와 데이터를 의미
  • 프로세스 : 프로그램이 메모리에 적재되어 실행 중인 상태를 의미
  • 코드 : 실행할 프로그램의 명령어
  • 데이터 : 프로그램이 사용하는 정적 데이터
  • 스택 : 함수 호출과 지역 변수를 위한 메모리 공간

참조

profile
https://www.pigpgw.cloud 로 이전합니다~

0개의 댓글