Post

Lecture 05: Machine-Level Programming I: Basics

Computer Systems: A Programmer's Perspective (CS:APP)

x86 Architecture


x86 아키텍처는 인텔이 개발한 명령어 집합 구조(Instruction Set Architecture, ISA)이다. 대표적인 CISC(Complex Instruction Set Computer) 아키텍처로, 수많은 기능과 복잡한 구조를 가진다.

Intel x86 Evolution: Milestones

NameYearTransistorsMHzDescription
8086197829K5-10최초로 x86을 사용한 16비트 프로세서
3861985275K16-33x86 최초로 32비트 아키텍처(IA-32) 사용
Pentium 4E2004125M2800-380064비트 아키텍처(x86-64) 사용
Core 22006291M1060-3500멀티 코어 프로세서
Core i72008731M1700-3900네이티브 쿼드 코어

싱글 코어의 클럭 향상에 따른 전력 소모와 발열이 한계에 다다르자, 코어 수를 늘려 성능을 향상시킨 멀티 코어 프로세서가 등장했다.

x86 Clones: Advanced Micro Devices (AMD)

인텔이 x86을 포기하고 완전히 새로운 64비트 아키텍처(IA-64)를 개발하는 동안 AMD는 x86의 64비트 확장 아키텍처(x86-64)를 발표하였고, x86 호환성을 갖춘 x86-64가 시장에서 승리하면서 오늘날 거의 모든 프로세서가 이를 사용하고 있다.


C, Assembly, Machine Code


Turning C into Machine Code

---
config:
    flowchart:
        htmlLabels: false
---
flowchart TD
    c1[/"p1.c"/]
    c2[/"p2.c"/]
    s1[/"p1.s"/]
    s2[/"p2.s"/]
    o1[/"p1.o"/]
    o2[/"p2.o"/]
    a[/"Static libraries (.a)"/]
    p[/"Executable program (p)"/]
    Compiler["Compiler (gcc -Og -S)"]
    Assembler["Assembler (gcc or as)"]
    Linker["Linker (gcc or ld)"]

    c1 & c2 --> Compiler --> s1 & s2 --> Assembler --> o1 & o2
    o1 & o2 & a --> Linker --> p

Compiling into Assembly

다음은 sum.c 파일의 일부이다.

1
2
3
4
5
6
long plus(long x, long y);

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

이를 gcc -Og -S sum.c와 같이 컴파일하면 생성되는 sum.s 파일을 통해 어셈블리 코드를 확인할 수 있다.

gcc 옵션에 대한 설명은 여기1 참고

1
2
3
4
5
6
7
sumstore:
        pushq   %rbx
        movq    %rdx, %rbx
        call    plus
        movq    %rax, (%rbx)
        popq    %rbx
        ret

Disassembling Machine Code

디스어셈블러(Disassembler)는 어셈블러와 반대로 기계 코드를 어셈블리 코드로 변환한다. gcc -Og sum.c -o sum과 같이 컴파일하여 sum 프로그램을 생성한 뒤, objdump -d sum을 입력하여 결과를 확인해 보자.

1
2
3
4
5
6
7
0000000000001196 <sumstore>:
    1196:       53                      push   %rbx
    1197:       48 89 d3                mov    %rdx,%rbx
    119a:       e8 ea ff ff ff          call   1189 <plus>
    119f:       48 89 03                mov    %rax,(%rbx)
    11a2:       5b                      pop    %rbx
    11a3:       c3                      ret


Assembly Basics


x86-64 Integer Registers

x86-64는 정수와 포인터를 저장할 수 있는 16개의 레지스터2를 가지고 있다.

64-bit registerLower 32 bitsLower 16 bitsLower 8 bits
%rax%eax%ax%al
%rbx%ebx%bx%bl
%rcx%ecx%cx%cl
%rdx%edx%dx%dl
%rsi%esi%si%sil
%rdi%edi%di%dil
%rsp%esp%sp%spl
%rbp%ebp%bp%bpl
%r8%r8d%r8w%r8b
%r9%r9d%r9w%r9b
%r10%r10d%r10w%r10b
%r11%r11d%r11w%r11b
%r12%r12d%r12w%r12b
%r13%r13d%r13w%r13b
%r14%r14d%r14w%r14b
%r15%r15d%r15w%r15b

Operand Types

TypeExample
Immediate$0x400, $-533
Register%rax, %r13
Memory(%rax)

Immediate는 상수 데이터를 의미한다. (상수 앞에 $를 붙임)

Moving Data

movq Source, Dest 명령은 Source의 값을 Dest에 복사한다. (mov + q: quad word)

SourceDestExampleC analog
ImmediateRegistermovq $0x4, %raxtemp = 0x4;
 Memorymovq $-147, (%rax)*p = -147;
RegisterRegistermovq %rax, %rdxtemp2 = temp1;
 Memorymovq %rax, (%rdx)*p = temp;
MemoryRegistermovq (%rax), %rdxtemp = *p;

한 메모리 위치에서 다른 메모리 위치로 직접 복사하는 것은 허용되지 않는다. 따라서 메모리에서 레지스터로 복사한 뒤, 해당 레지스터의 값을 메모리에 복사하는 두 단계를 거쳐야 한다.

Simple Memory Addressing Modes

ModeRegisterMemory
Normal(R)Mem[Reg[R]]
DisplacementD(R)Mem[Reg[R] + D]
  • (R): 레지스터 R의 값에 해당하는 메모리 주소 참조 (포인터 역참조와 동일)
  • D(R): (R)에서 변위 D만큼 이동한 메모리 주소 참조

Example of Simple Addressing Modes

1
2
3
4
5
6
void swap(long *xp, long *yp) {
    long t0 = *xp;
    long t1 = *yp;
    *xp = t1;
    *yp = t0;
}
1
2
3
4
5
6
swap:
        movq    (%rdi), %rax
        movq    (%rsi), %rdx
        movq    %rdx, (%rdi)
        movq    %rax, (%rsi)
        ret

포인터가 가리키는 주소와 해당 주소에 저장된 값이 다음과 같을 때,

NameAddress (p)Value (*p)
xp0x120123
yp0x100456

각 레지스터에는 다음과 같은 값이 저장된다.

RegisterValue
%rdi0x120
%rsi0x100
%rax123
%rdx456

Complete Memory Addressing Modes

RegisterMemory
D(Rb, Ri, S)Mem[Reg[Rb] + S*Reg[Ri] + D]
  • D: 변위
  • Rb: 베이스 레지스터
  • Ri: 인덱스 레지스터 (%rsp 제외)
  • S: 스케일

배열 참조를 구현하는 데 유용한 형태이다.

Address Computation Examples

RegisterValue
%rdx0xf000
%rcx0x0100
ExpressionAddress computationAddress
0x8(%rdx)0xf000 + 0x80xf008
(%rdx, %rcx)0xf000 + 0x1000xf100
(%rdx, %rcx, 4)0xf000 + 4*0x1000xf400
0x80(, %rdx, 2)2*0xf000 + 0x800x1e080

Load Effective Address (LEA)

leaq Source, Dest 명령은 주소 계산(Source)을 수행하여 결괏값을 레지스터(Dest)에 쓴다. 다음 예시와 같이 산술 연산을 수행하는 데 사용되기도 한다.

1
2
3
long m12(long x) {
    return x * 12;
}
1
2
leaq    (%rdi,%rdi,2), %rax  ; ret = x + x*2
salq    $2, %rax             ; ret <<= 2

Some Arithmetic Operations

FormatComputation
addq Source, DestDest += Src
subq Source, DestDest -= Src
imulq Source, DestDest *= Src
salq Source, DestDest <<= Src
shlq Source, DestDest <<= Src
sarq Source, DestDest >>= Src (산술 시프트)
shrq Source, DestDest >>= Src (논리 시프트)
xorq Source, DestDest ^= Src
andq Source, DestDest &= Src
orq Source, DestDest |= Src
incq DestDest += 1
decq DestDest -= 1
negq DestDest = -Dest
notq DestDest = ~Dest

Arithmetic Expression Example

1
2
3
4
5
6
7
8
9
long arith(long x, long y, long z) {
    long t1 = x + y;
    long t2 = z + t1;
    long t3 = x + 4;
    long t4 = y * 48;
    long t5 = t3 + t4;
    long rval = t2 * t5;
    return rval;
}
1
2
3
4
5
6
7
8
arith:
        leaq    (%rdi,%rsi), %rax
        addq    %rdx, %rax
        leaq    (%rsi,%rsi,2), %rdx
        salq    $4, %rdx
        leaq    4(%rdi,%rdx), %rdx
        imulq   %rdx, %rax
        ret

컴파일러는 최대한 빠른 명령어를 사용하여 요구 사항을 구현하기 위해 여러 가지 방법을 시도한다. 그 결과 저수준에서의 연산 순서가 고수준에서 지정한 순서와 달라질 수 있다.


References


Footnote

이 글은 저작자의 CC BY-SA 4.0 라이선스를 따릅니다.