[System Programming] 1. Machine Basics

윤호·2022년 10월 14일
0

System Programming

목록 보기
2/7
post-thumbnail

Machine basics

우리가 code를 작성할 때는 user level에서 이해하기 쉬운 programming language(ex. C, C++, python, java, etc)를 활용합니다. 하지만, CPU와 같은 machine level에서는 우리가 보는 language의 형태로 연산을 진행할 수 없고 0과 1로 이루어진 binary machine code를 통해 처리합니다. 따라서, computer에 대해 이해하기 위해서는 CPU 등의 logic에서 binary를 어떻게 처리하는 지 이해하는 것이 필요합니다.

본격적으로 들어가기에 앞서 몇 가지 용어에 대해 설명하려 합니다.

  • Architecture(ISA: instruction set architecture): programmer의 관점에서 바라본 system의 description, system의 functionality를 정의한 것
    • Ex) instruction set specification, registers
  • Microarchitecture: architecture의 hardware level implementation
    • Ex) cache sizes, core frequency, pipelines
  • Machine code: processor가 실행할 수 있는 byte-level programs
  • Assembly code: machine code의 text representation
  • Programmer-Visible State: programmer의 관점에서 본 processor의 현재 상태를 표현한 것
    • Ex) PC(Program Counter), register file, condition codes, memory

현재 작성하고 있는 System Programming 시리즈는 Intel의 x86-64 architecture를 기반으로 하여 작성하고 있습니다.


Assembly characteristics

Registers

x86-64 architecture에서 사용하는 register는 다음과 같습니다. 표 위의 숫자는 각 register의 size를 표시한 것이고, 오른쪽은 각 register들의 주요 옹도를 표시한 것입니다. 이들은 빠른 data 연산을 위해 DRAM이나 cache에 포함되지 않고 독립적으로 존재합니다.

register 중에서 특수한 목적으로 사용되는 register로는 %rsp와 %rip(그림에는 표시되지 않은 register)가 있습니다. %rsp는 stack pointer로, 나중에 설명할 virtual memory 개념에서 stack의 위치를 표시하기 위한 stack pointer로 사용합니다. %rip는 program counter로, 다음에 수행할 instruction이 있는 address를 저장하는 register입니다. 그 외의 대부분의 register는 temporary data를 저장하거나 function의 return value를 저장하는 등의 용도로 사용합니다.

Operations

assembly에서 사용하는 operation은 크게 다음과 같이 분류할 수 있습니다.

  • memory와 register 사이의 data transfer
    • Load: memory로부터 register로 data를 이동
    • Store: register로부터 memory로 data를 이동
  • register, memory data를 이용한 arithmetic / logical function의 수행
  • Transfer control
    • unconditional jump
    • conditional jump
    • indirect branch

x86-64 architecture에서 예를 들면, load와 store를 수행하는 instruction으로 mov 등, arithmetic function을 수행하는 instruction으로 add, sub 등, transfer control로 jmp, je 등이 있습니다. x86-64의 특징으로 instruction에 suffix가 붙는 경우가 있는데(movl, movq 등), 이는 instruction의 operand가 가지는 data type을 의미합니다. 예를 들어 l은 4-byte word를, q는 8-byte word를 의미합니다.

assembly language에서 operand를 작성할 때에는 기본적으로 Command, Source, Destination의 순서로 작성합니다. 또한, constant value를 작성할 때에는 $ 기호를 이용하며 register name을 작성할 때에는 %를 활용합니다. 주어진 value를 address로 가지는 memory 내의 value를 호출하고자 할 때에는 소괄호를 활용해 표현해줍니다. 단, system에서 memory-to-memory로 data를 move하는 operation은 작성할 수 없다는 점을 주의해야 합니다. 예시로 아래 5개의 movq operation을 C language로 표현하면 각각 다음과 같은 의미를 가지고 있습니다.

assembly
C analog
movq $0x4,%raxtemp = 0x4;
movq $-147,(%rax)*p = -147;
movq %rax,%rdxtemp2 = temp1;
movq %rax,(%rdx)*p = temp;
movq(%rax),%rdxtemp = *p

위의 code에 이어서, 더 general한 memory addressing form은 D(Rb, Ri, S)의 형태입니다. 이는 Mem[Reg[Rb]+S*Reg[Ri]+D]를 의미합니다. 이해를 돕기 위해 %rdx의 value가 0xf000이고, %rcx의 value가 0x100일 때의 address comutation 결과를 작성해 두었습니다.

Expression
Address Computation
Address
0x8(%rdx)0xf000 + 0x80xf008
(%rdx, %rcx)0xf000 + 0x1000xf100
(%rdx, %rcx, 4)0xf000 + 4*0x1000xf400
0x80(, %rdx, 2)2*0xf000 + 0x800x1e080

Arithmetic operation 중 addq, subq 등은 동작을 쉽게 연상할 수 있기 때문에 추가적인 설명을 하지 않고 한 가지만 짚어보고자 합니다. 먼저 x86-64 architecture에는 lea라는 operation이 있습니다. lea는 source의 address를 destination에 넣어주기 위한 operation입니다. assembly code의 form이 mov와 유사하기 때문에 혼동을 일으킬 여지가 있기 때문에 예시를 통해 동작을 살펴보겠습니다.

// value of %rdi is $0x100
movq (%rdi, %rdi, 2), %rax		// %rax = Mem[%rdi + 2*%rdi] = Mem[$0x300]
leaq (%rdi, %rdi, 2), %rax		// %rax = %rdi + 2*%rdi = $0x300

예시에서 볼 수 있는 차이점은, mov는 memory에서 data를 가져와 %rax에 넣어주는 동작을 하지만 lea%rdi의 value를 연산한 결과를 %rax에 넣는, 즉 memory의 address를 넣어주는 동작을 한다는 것입니다. 따라서 register의 value에 addition, multiplication을 진행한 결과를 구하기 위해서 lea를 사용할 수 있습니다.


Turning C into object code

이번에는 C code로 작성한 파일을 compile할 때 진행되는 과정에 대해 살펴보고자 합니다. 두 개의 c file p1.c, p2.c 에 대해서 이를 gcc로 compile할 때 진행되는 과정은 다음 그림과 같습니다.

먼저 두 개의 c code를 각각 compiler가 compile하여 assembly code(p1.s, p2.s)로 변환해준 후, 이를 다시 assembler가 object program(p1.o, p2.o)로 변환해줍니다. 마지막으로 code에서 include한 library와 두 개의 object program을 linker가 묶어줘 하나의 executable program을 만들어줍니다. c code와 assembly code는 text file이며 object와 executable program은 binary file입니다. linker에 관한 부분은 다음에 짚어보도록 하고 지금은 compiler와 assembler에 의한 결과를 살펴볼 것입니다.

c code를 assembly code로 변환하는 것은 gcc compiler에 의해 수행됩니다. compile 결과를 이해하기 쉽도록 하기 위해 다음과 같은 sum.c file에 대한 결과를 보고자 합니다.

long plus(long x, long y);

void sumstore(long x, long y, long *dest) {
	long t = plus(x, y);
    *dest = t;
}

위의 code를 compile하기 위해 linux command에 gcc -Og -S sum.c를 입력해주면 gcc compiler에 의해 다음과 같은 assemblly code(sum.s)를 얻을 수 있습니다.

	.globl	sumstore
    .type	sumstore, @function
sumstore:
.LFB35:
	.cfi_startproc
	pushq	%rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    movq	%rdx, %rbx
    call	plus
    movq	%rax, (%rbx)
    popq	%rbx
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE35:
	.size	sumstore, .-sumstore

위의 assembly code는 gcc version과 compiler setting에 따라 다르게 나타날 수 있다는 점은 유의해야 합니다. code에서 '.'으로 시작하는 line은 assembler와 linker를 위한 guide를 목적으로 만들어진 directive입니다. 이들을 제외한 나머지 line들이 assembly code로 작성된 부분들입니다. 해당 assembly code를 assembler를 통해 다시 object code로 변환해주면 다음과 같은 형태의 object file(sum.o)를 얻을 수 있습니다.

0x0000000: 53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3

맨 앞의 0x0000000는 memory address이며, 해당 address부터 code가 작성되어 있음을 알립니다. object code 내에서는 아직 address가 배정되지 않았지만, 후에 linker를 통해 executable file로 만들어주면 address가 지정됩니다. instruction 각각 1, 3, 5 byte로 구성되어 있으며 이는 architecture 내에서 정의되어 있는 binary encoding을 따릅니다. 위의 object file을 역으로 해석하기 위해 disassemble을 진행하기도 합니다. disassemble은 object file 뿐만 아니라 executable program에 대해서도 진행해줄 수 있습니다. 아래 code는 위의 object code와 linker를 통해 만들어진 executable file의 <sumstore> 부분을 disassemble한 결과입니다.

// disassembly of object file
0000000000000000 <sumstore>:
	0:	53				push	%rbx
    1:	48 89 d3		mov	%rdx, %rbx
    4:	e8 00 00 00 00	callq 9 <sumstore+0x9>
    			5:	R_X86_64_PLT32		plus-0x4
    9:	48 89 03		mov	%rax, (%rbx)
    c:	5b				pop	%rbx
    d:	c3				retq

// disassembly of executable file
0000000000400595 <sumstore>:
	400595:	53				push	%rbx
    400596:	48 89 d3		mov		%rdx, %rbx
    400599: e8 f2 ff ff ff	callq	400590 <plus>
    40059e: 48 89 03		mov		%rax, (%rbx)
    4005a1:	5b				pop		%rbx
    4005a2:	c3				retq

object file에서는 정해지지 않았던 <sumstore><plus>의 address가 executable file에서는 결정되어 있는 것을 확인해볼 수 있습니다. 이러한 linking 과정은 후에 더 자세히 살펴보도록 하겠습니다.


Reference
Computer System: A Programmer's Perspective, 3rd ed (CS:APP3e), Pearson, 2016

0개의 댓글