ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 5 : Machine-Level Programming 1 (Basis)
    컴퓨터 시스템 개론 2022. 3. 30. 21:06
    728x90

    Compilation System

    C(high-level)로 작성된 프로그램이 CPU가 읽을 수 있는 binary cord로 변환되는 과정입니다.

     

    Turning C into Object Code

    파일 p1.c와 p2.c를 리눅스 쉘에서 컴파일한다고 가정해보자

    리눅스 명령어에 gcc -Og -o p p1.c p2.c 를 입력합니다.

    gcc는 gcc 컴파일러를 가리킵니다.

    -Og는 최적화의 단계를 적용하는 컴파일러를 가리킵니다. -O1, -O2, -O3 까지 있으며

    각각 최적화 LEVEL 1, 2, 3를 의미합니다. LEVEL이 높아질 수 록 최적화의 정도가 높아집니다.

    하지만 O3 정도되면 최종 실행파일이 더빠르게 실행되지만 source code와 machine code의 관계를 이해하기

    어렵습니다. 따라서 컴파일 하는데 오래걸리고 디버깅도 쉽지 않습니다.

    -o p는 c파일을 p라는 결과물로 전달

    gcc 명령어는 source code를 실행가능한

    파일로 바꾸는 프로그램을 만듭니다.

    1) c 전처리기는 #include로 명시된 파일을

    포함시켜 source code를 확장하고 

    #define으로 명시된 매크로로 확장합니다.

    2) 컴파일러는 p1.c, p2.c를 p1.s, p2.s의 

    어셈블리 코드를 생성합니다.

    3) 어셈블리 코드는 p1.s, p2.s를 p1.o, p2.o의 

    binary object code파일로 변환합니다.

    마지막으로 linker가 구현된 라이브러리 파일과 두 object code 파일을 병합시켜 실행가능한 프로그램 p를 

    생성합니다.

     

    Architecture (ISA : instruction set architecture)

    ISA는 소프트웨어와 하드웨어 간의 interface입니다.

    소프트웨어가 하드웨어의 운영 및 storage를 호출하고 access하는 방법에 대한 이해입니다.

    ISA 밑에 많은 하드웨어가 있는데 이러한 복잡성을 

    모두 하나하나 전부 보여주는 것이 아니라

    어느정도 필요한 부분을 소프트웨어에 보여주는것이 ISA입니다.

    즉, ISA까지 알면 기본적으로 밑에 있는 디테일은 볼 필요없이

    source code가 machine 코드로 어떻게 바뀌는지 볼 수 있습니다.

     

     

     

     

     

    PC(Program Counter)는 다음 실행될 instruction의 address를 저장하는 register 입니다. 

    이게 x86-64에서 %rip라는 register가 역할을 수행합니다.

    Condition Codes register는 ALU가 어떤 연산을 했을 때 연산의 결과가 overflow인지 carry를 했는지 양수인지

    음수인지 저장하는 역할을 해 conditional jump를 하게 됩니다. 

     

    Architecture (ISA)

    어셈블리 machine code를 이해하거나 작성해야하는 Processor 설계 부분입니다.

    ex) register

    Microarchitecture

    architecture의 구현입니다.

    ex) 캐시(cache) size, core frequency

     

    Machine Code

    byte level의 이진 코드 형태입니다. Processor가 수행하는 코드입니다.

    기본적인 opration만 수행합니다.(+, -, +, /) data move, conditional branch, function call같은 기능을 수행합니다.

    메모리는 virtual addressing합니다. 이는 CPU 하드웨어인 MMU가 virtual address를 physical address로 변환해

    메모리로 access합니다.

     

    Assembly Code

    machine code와 매우 비슷합니다. 기계 코드보다는 읽을 수 있는 형태로 되어있습니다. 

     

    Code Example

    sum.c 라는 파일입니다.

    shell에 gcc -Og -S sum.c 라는 명령어를 입력하면

    sum.s 라는 어셈블리 파일이 생깁니다.

     

    주의 : 이러한 결과값은 gcc 혹은 compiler의 version에 따라 다르게 나올 수 있습니다.

     

    Machine Instruction Example

    C code

    *dest = t; //t = 10

    t값을 포인터 dest가 가리키는 곳에 저장합니다.

    위 code를 assembly 언어로 바꾸면

     

    Assembly

    movq %rax,   (%rbx)

    mov는 움직이라는 의미입니다.

    q는 8 byte입니다. (b = 1, w = 2, l = 4)

    t는 register %rax

    dest는 register %rbx

    *dest는 메모리 M[%rbx]

    즉, %rax(t)에서 %rbx에 위치로 8 byte만큼 옮긴다 라는 의미입니다.

     

    Object Code

    0x40059e :  48 89 03

    48, 89, 03 각각  3 byte씩 instruction입니다.

    0x40059e라는 주소값에 저장하라는 의미입니다.

     

    Disassembling Object Code

    기계 코드 파일의 내용을 보려면, 디셈블러(disassembler)라고 불리는 프로그램이 중요합니다.

    object code를 검사하는 데 유용한 도구입니다.

    이 프로그램은 기계 코드로부터 어셈블리 코드와 비슷한 형태를 생성합니다.

    일련의 bit 로 되있는 명령의 패턴을 분석합니다.

    a.out(완전 실행 파일) 또는 o 파일에서 실행할 수 있습니다.

    shell 에서 objdump -d sum 을 입력하면

    위와 같이 나옵니다.

     

    sumstrore의 object code입니다.

    주소 0x0400595에서 시작합니다.

    총 14 byte입니다.

    각 명령어는 1, 3 또는 5 byte를 차지합니다.

     

     

     

     

     

     

     

     

     

    이것을 Disassemble 한 것입니다.

    disassembler는 byte를 검사하고 assembly 소스를 재구성합니다.

     

    x86-64 Integer Register

    x86-64 bit 컴퓨터의 CPU에는 16개의 register가 저장되있습니다.

    기본적으로 64 bit이며 레지스터들은 1, 2, 4 byte로 분해가능합니다. 

    %eax ~ %ebp까지의 레지스터를 확대한 것입니다.

    %ax ~ %bx까지의 레지스터들 각각은 다시 반으로 쪼개져 8 bit(1 byte) 레지스터 8개를 이루게 됩니다.

    각 레지스터의 이름은 그 용도와 밀접한 관련이 있습니다. 

    예를들어 %rsp는 스택 포인터로 사용되는 레지스터입니다. 

    위 그림에서 오른쪽에 해당 레지스터의 용도가 나타나 있습니다.

    Moving Data

    movq Source, Dest

    movq는 source에 해당하는 값을 dest가 나타내는 공간에 이동(저장) 시킵니다. 

    source와 dest자리에 들어갈 수 있는 값의 유형은 다음 3가지 입니다.

    • 상수 (Immediate)

    레지스터나 메모리에 있는 값이 아니라, 명령어 bit 자체에 적는 값을 의미합니다.

    $ 기호를 붙여서 표현합니다. 이러한 상수들은 명령어 bit 자체에 1, 2, 또는 4 byte로 인코디이 됩니다.

    상수는 dest자리에 올 수 없습니다. 

    ex) $0x400, $-533

    • Register 레지스터

    위에서 봤던 16개 레지스터들 중 하나를 의미합니다. 

    단 %rsp는 스택 포인터로만 사용되기 때문에 제한적입니다.

    ex) %rax, %r13

    • Memory 메모리

    레지스터에 의해 주어지는 메모리 주소에서 시작하는 연속적인 8개의 byte를 의미합니다.

    x86-64는 메모리의 값을 참조하기 위한 주소지정방식(Addressing Mode)이 굉장히 다양한데, 

    이에 대해서는 나중에 살펴보겠습니다.

    ex) (%rax) : %rax가 가리키는 메모리 주소에서 시작하는 연속적인 8개의 byte

     

    중요한점은 메모리와 메모리 사이의 데이터 이동은 불가능합니다.

    따라서 메모리에 값을 읽어와서 레지스터에 저장한 뒤, 그것을 다시 메모리에 적도록 해야합니다.

    Simple Memory Addressing Modes

    메모리에 접근하기 위한 주소지정방식(Addressing Mode)이 굉장히 다양합니다.

    (%rax) 처럼 단순한게 아니라 훨씬 복잡하고 다양한 방식으로 메모리에 접근이 가능합니다.

     

    Normal 버전 : (R) Mem[Reg[R]]

    레지스터 R은 메모리 주소를 가리킵니다.

    ex) monq (%rcx), %rax

     

    Displacement 버전 : D(R) Mem[Reg[R] + D]

    레지스터 R은 base memory 주소를 가리킵니다. 

    displacement(offset) D는 base address 부터 얼마나 떨어져 있는지 가리킵니다.

    ex) movq 8(%rsp), %rdx

     

    EX)

     

    Complete Memory Addressing Modes

    D(Rb, Ri, S) => Mem[Reg[Rb] + S * Reg[Ri] + D]

    Rb는 메모리 시작 주소를 나타내는 base register, 명시되지않으면 0입니다.

    D는 메모리 시작 주소로부터 offset을 나타내는 상수 (1, 2 또는 4 byte)

    Ri는 인덱스 레지스터 (%rsp는 제외), 명시되지않으면 0입니다.

    S는 인덱스 레지스터의 값을 몇 배하여 더할지 결정하는 Scale 상수(1, 2, 4, or 8) 명시되지않으면 1입니다.

     

    위의 기본 형태를 변형한 형식입니다.

    (Rb,Ri)  Mem[Reg[Rb]+Reg[Ri]] (S=1,D=0)

    D(Rb,Ri)  Mem[Reg[Rb]+Reg[Ri]+D] (S=1)

    (Rb,Ri,S)  Mem[Reg[Rb]+S*Reg[Ri]] (D=0)

     

    Address Computation Instruction

    leaq Src, Dst

    leaq는 메모리에 직접 접근하지 않고 특정 메모리 주소를 계산해내는 명령어입니다.

    Src에는 위에서 본 주소지정방식의 표현식이 들어갑니다. 그렇게 계산된 주소 값이 Dst가 나타낸는 공간에

    저장이 됩니다.

    leaq의 기본적인 용도는 다음과 같은 C언어 코드를 번역하는 데 있습니다. 

    메모리에 접근하지 않고 원하는 메모리 주소를 계산해 내기 때문입니다.

    P = &X[i]

    또는 x+k*y (k = 1, 2, 4 or 8) 형태의 표현식을 계산하는 데에도 사용이 됩니다.

     

    Some Arithmetic Operations

    산술 & 논리 연산 명령어 들은 다음과 같습니다.

    피연산자 개수가 2

    피연산자 개수가 1

    등등 더많은 instruction이 있습니다.

     

    ex) c언어 코드를 컴파일러가 위의 명령어를 최적화하여 변역하였습니다. '최적화'

    인자로 오는 변수 x, y, z는 각각 순서에 따라

    레지스터에 할당됩니다.

    x = %rdi

    y = %rsi

    z = %rdx

     

     

     

     

     

     

    long t1 = x + y; leaq (%rdi, %rsi), %rax => %rax = %rdi + %rsi 에 따라 %rax = t1, %rax = x + y 입니다.

    long t2 = z + t1; addq (%rdx, %rax) => %rax = %rax + %rdx 에 따라 %rax = x + y + z 입니다.

    leaq (%rsi, %rsi, 2), %rdx => %rdx = %rsi + 2 * %rsi 에 따라 %rdx = y + 2y = 3y입니다.

    위 코드는 c코드에 없습니다. 이는 컴파일러 자체 판단입니다.

    변수 z는 더이상 사용되지 않기 때문에 z에 레지스터를 지정해주면 비효율적입니다. 

    따라서 변수 z를 저장했던 %rdx에다가 후에 사용될 3y를 저장하는 것입니다.

    long t4 = y * 48; salq %4, %rdx => shift연산은 제곱과 곱셈을 이용해서 %rdx = %rdx * 2^4입니다.

    이에 따라 %rdx = 3y * 16 = 48y입니다.

    long t5 = t3 + t4; leaq 4(%rdi, %rdx), %rcx => %rcx = 4 + %rdi + %rdx에 따라 %rcx = 4 + x + 48y입니다.

    long rval = t2 * t5; imulq %rcx, %rax => %rax = %rax * %rcx에 따라 %rax = (x+y+z) * (4+x+48y)입니다.

     

    위의 코드에서 변수는 총 9개 사용했지만 실제 사용된 레지스터는 5개입니다.

    레지스터는 한정된 하드웨어 자원이기 때문에 효율적이게 적은 개수로 사용하는 편이 좋습니다.

     

    '컴퓨터 시스템 개론' 카테고리의 다른 글

    Chapter 7 : Machine-Level Programming 3 (Procedure)  (0) 2022.04.25
    Chapter 6 : Machine-Level Programming 2 (Control)  (0) 2022.04.06
    Chapter 3 : Float  (0) 2022.03.27
    Chapter 2 (Bits, Bytes, and Integers)  (0) 2022.03.22
    chapter 1  (0) 2022.03.05
Designed by Tistory.