Assembly: 두 판 사이의 차이

youngwiki
편집 요약 없음
53번째 줄: 53번째 줄:
#* Linking을 통해서 executable file이 된다.
#* Linking을 통해서 executable file이 된다.
# Executable file: 최종적으로 실행가능한 결과 파일이다.
# Executable file: 최종적으로 실행가능한 결과 파일이다.
==Operand types==
Assembly에는 기본적으로 아래의 3개의 기본 자료형이 존재한다.
* Intermediate: 상수 정수 값을 의미한다. address와 구분하기 위해 상수값 앞에 $를 붙여서 표시한다.
** ex) $0x400, $-533
* Register: register들을 의미한다.
** ex) %rax, %r13
* Memory: 메모리 내의 address가 지정하는 consecutive byte value를 의미한다.
** ex) (%rax), 0x1000<ref>mem에 해당하는 정수에는 $ 표시를 사용하지 않는다.</ref>


==Data Move Instruction: mov==
==Data Move Instruction: mov==

2025년 4월 10일 (목) 01:01 판

상위 문서: 컴퓨터 시스템

개요

C 프로그램을 작성하고 컴파일하면 해당 코드는 assembly code로 변환된다.(이렇게 변환된 코드는 나중에 machine code로 다시 변환된다.) Assembly는 컴퓨터가 이해할 수 있는 code의 형태이다. 즉, Assembly를 공부한다는 것은 컴퓨터가 내부적으로 어떻게 동작하는지를 공부한다는 것이다. 이를 공부해야 하는 이유는 각각의 사람마다 알아야 하는 abstraction의 수준이 다르기 때문이다. 예를 들어 C 프로그래머는 C언어에 대해서만 알아도 된다. Compiler writer(Assembly programmer)는 또한 ISA[1]에 대해서 알아야 한다. 이는 x86-64에서 어떤 종류의 instruction이 사용 가능하고, 해당 instruction들이 CPU에서 정확히 무엇을 하는지 알려준다. 마지막으로 CPU designer는 디지털 회로, logic gate와 같은 microarchitecture에 대해서도 알아야 한다.

Inside Our Computer

CPU와 Main Memory는 프로그램을 구동하는 가장 핵심적인 두가지 요소이다. Assembly code는 register들을 통해 프로그램의 실행을 직접적으로 제어한다. 즉 assembly 명령어에 대해 더욱 잘 이해하기 위해서는 register에 대한 개념이 잡혀있어야 한다.

CPU work

CPU는 다음과 같은 과정을 따라서 동작한다.

  1. CPU가 메모리로부터 machine instruction을 fetch한다.
    • Program Counter(PC)는 해당 instruction을 fetch하기 위해서 사용되는 주소를 저장하는 register이다.
    • 이때 메모리는 Code와 Data 모두를 저장하고 있다.
  2. fetch된 instruction이 CPU에게 무엇을 해야할지를 알려준다.
    • 예를 들면 두 register를 더하거나, register로부터 메모리로 데이터를 옮기거나 하는 등의 행동이 있다.
    • Assembly instruction은 machine instruction과 1대1로 대응되며, 사람이 해석하기 편하도록 가독성이 좋아진 버전이다.
  3. 해당 instruction의 execution 후에 PC는 자동적으로 next instruction에 대한 주소를 업데이트한다.
    • 이때 jump와 같은 명령어는 PC가 특정한 address를 저장하도록 직접적으로 명령하기도 한다.
    • 해당 작업을 수행한 수, CPU는 1부터 다시 작업을 반복한다.

Architecture

ISA와 microarchitecture라는 두가지 의미를 가진다.

  • Instruction set architecture(ISA): CPU design이 반드시 따라야 하는 abstract한 구체화
    • assembly code를 분석하거나 쓰기 위해서는 CPU의 특정 부분의 design을 반드시 알아야 한다.[2]
    • IA32, x86-64가 ISA의 대표적인 예시들 중 하나이다.
  • Microarchitecture: hardware 수준에서 ISA가 어떤 방식으로 구현되는지에 대한 구체적인 방법을 의미한다.

하지만 현재 문서에는 주로 ISA의 의미를 가지는 architecture에 대해서 설명을 할것이다.

Intel x86

x86은 Intel에 의해 개발된 architecture 시리즈이다. 32bit에서 64bit로의 word 크기의 확장과 같이 수많은 진보가 있어왔으며, 얼마 전까지만 해도 laptop/desktop/server 시장을 쥐락펴락했다. 최근에는 AMD architecture에 의해서 시장에서의 선두를 빼았겼지만, 아직 많은 system들이 x86 system을 사용하므로, YOUNGWIKI의 서술은 기본적으로 x86 기준으로 한다.

x86은 종종 두가지 의미로 사용되는데, 첫번째는 앞서 말한 architecture 시리즈이고, 다른 하나는 IA32라는 특정한 architecture를 말하는 것이다. 하지만 YOUNGWIKI에서는 x86 중에서도 x86-64 architecture에 초점을 두어 설명한다.

From C source to Machine Code

gcc p1.c p2.c -0 p.bin


위 코드를 Linux shell에 입력한다면 아래와 같은 복잡한 과정을 거쳐 p.bin이라는 이름의 executable file이 생성된다.

  1. C source code: p1.c, p2.c라는 text 형태로 존재하는 c code이다.
    • 프로그래머가 직접 프로그램하는 파일 형식이다.
    • compile을 통해서 assembly code가 된다.
  2. Assembly code: p1.s, p2.s라는 text 형태로 존재하는 assembly code이다.
    • Machine code와 1대1로 대응되는 text 형식의 assembly로 작성된 code이다.
    • Assmble을 통해서 object file이 된다.
  3. Object file: p1.o, p2.라는 binary form으로 존재하는 object file이다.
    • Linking을 통해서 executable file이 된다.
  4. Executable file: 최종적으로 실행가능한 결과 파일이다.

Data Move Instruction: mov

Operand combination for mov

mov instruction 할당에 해당하는 명령이며, 다음의 형식을 따른다.

mov Source, Destination #e.g) mov %rax, %rbx

mov뒤에는 접미사가 붙어 operand의 size를 제한할 수 있다.

  • b= 1 byte, w= 2 bytes, l= 4 bytes, q= 8 bytes

또한 mov는 오른쪽 이미지와 같은 피연산자의 조합이 가능하다.

Byte Extension with movz/movs

movz/movs instruction은 할당을 할 때 extension의 과정까지 거치는 기능을 제공한다.
movz는 zero extension을 진행한다. 이때 suffix(b/w/l/q)를 사용하여 얼마나 extension을 할지 지정할 수 있다.

movzbw %bl, %ax    #zero-extend 1byte to 2byte

movs는 sign extension을 진행한다. 마찬가지로 suffix(b/w/l/q)를 사용하여 얼마나 extension을 할지 지정할 수 있다.

movslq %ebx, %rax    #sign-extend 4byte to 8byte

Partial Access on Register

mov 명령어를 통해서 register에 부분적으로만 접근할 수 있다. 예를 들어 $rax = 0x1122334455667788이라고 하면, 다음과 같이 register에 부분적인 접근이 가능하다.

mov $1, $al      # %rax: 0x1122334455667701
mov $1, $ax      # %rax: 0x1122334455660001
mov $1, $eax     # %rax: 0x0000000000000001

이때 주의할 점은 x86-64에서는 32비트 register(%eax, %ebx 등)에 값을 쓰면, 해당 64비트 register의 상위 32비트를 0으로 클리어하도록 CPU가 자동으로 처리하게 설계되어있다는 것이다.

Memory access by mov

mov는 mem의 데이터를 가지는 register를 통해서 해당 mem이 가리키는 주솟값이 가리키는 메모리 공간에 접근할 수 있도록 한다. 예를 들어

movl 0x$4142, (%rax)

는 %rax가 가리키는 메모리 공간을 4byte 정수 0x4142로 업데이트하도록 한다. 이는 mov에 suffix l이 붙었기 때문이며, 이 경우 suffix l은 누락되어선 안된다. 만약 suffix가 변경되면, 해당 코드의 동작 또한 변경된다. 이를 도식화하여 나타내면 다음과 같다.

위에서는 4byte 정수 0x4152가 little endian 방식으로 저장되어, LSB에 해당하는 0x42가 0x100에 저장되는 것을 볼 수 있다.

mem register에 있는 데이터는 특정 메모리 공간의 시작점을 가리킨다. 이때 다음과 같은 레지스터와 정수가 존재한다고 하자.

  • Rb: Basic Register
  • Ri: Index Register
  • S: Scale Factor (1,2,4,8 중 하나이다.)
  • D: Constant Diplacement Value

이는 메모리 공간의 특정한 시작점을 지정하기 위해 다음과 같이 사용된다.

  1. D(Rb, Ri, S) = Mem[Reg[Rb] + Reg[Ri] * S + D]
  2. D(Rb, Ri) = Mem[Reg[Rb] + Reg[Ri] + D]
  3. (Rb, Ri, S) = Mem[Reg[Rb] + Reg[Ri] * S]
  4. (Rb, Ri) = Mem[Reg[Rb] + Reg[Ri]]
  5. (, Ri, S) = Mem[Reg[Ri] * S]

위의 표기법은 배열의 특정한 index에 위치한 entry를 찾는 방법과 상당히 유사하며, 실제로도 배열에 접근할 때 유용하다. 예를 들어 다음과 같은 식은 0x20 + %rbx + %rcx * 4에 해당하는 주솟값에 접근할 수 있도록 한다.

  • mov 0x20(%rbx, %rcx, 4), %rax

Arithmetic Instruction

다음은 몇가지 대표적인 arithmetic 명령어이다.

add     %rax, %rbx    # %rbx = %rbx + %rax
sub     %rax, %rbx    # %rbx = %rbx - %rax
imul    %rax, %rbx    # %rbx = %rbx * %rax
inc     %rax          # %rax = %rax + 1
dec     %rax          # %rax = %rax - 1
neg     %rax          # %rax = -%rax

이때 주요한 특징은 음수를 표현하는 방식을 2의 보수법을 채택해 signed, unsigned 정수의 연산이 구분되지 않는다는 것이다. 또한 다양한 피연산자 형식을 사용하여 위의 arithmetic 연산자를 통해서도 메모리에 접근하거나 하는 등의 행동을 취할 수 있다. 이는 다음 예시와 같다.

  • addq $1, (%rax) #해당 메모리 주소에 위치하는 값을 업데이트

또한 add %eax, %ebx와 같은 명령은 %ebx += %eax와 같이 작동을 한 후, mov 명령어에서 그랬던 것처럼 %rbx의 상위 4byte를 0으로 채운다.

Logical Instruction

다음은 몇가지 대표적인 logical 명령어이다.

shr     %rax, %rbx    # %rbx = %rbx >> %rax    ; Logical right shift
sar     %rax, %rbx    # %rbx = %rbx >> %rax    ; Arithmetic right shift
shl     %rax, %rbx    # %rbx = %rbx << %rax    ; Left shift
xor     %rax, %rbx    # %rbx = %rbx ^ %rax
and     %rax, %rbx    # %rbx = %rbx & %rax
or      %rax, %rbx    # %rbx = %rbx | %rax
not     %rax          # %rax = ~%rax

만약 shift 연산자의 2번째 인자가 누락되었다면, 1로 간주한다.

lea Instruction

lea 명령어는 본래 pointer 계산을 위해 만들어졌다. Syntax는 mov와는 그 행동이 다르다. 예를 들어서 다음의 C code는 lea를 통해서 표현될 수 있다.

int* a = &b[c];
long a = 3 * b;
lea (%rbx, %rcx, 4), %rax      #int* a = &b[c];
lea (%rbx, %rbx, 2), %rax      #long a = 3 * b;, 정수를 계산하는 데에도 이용될 수 있다.

위에서는 포인터 자료형에 계산된 주솟값을 저장하기 위해서 lea 명령어가 이용된 것을 볼 수 있다. 이때 mov와의 주요한 차이점은 lea가 주어진 주솟값을 단순히 대입하는데에서 끝나는 반면, mov는 주어진 주솟값을 주어진 주솟값을 통해 실제로 해당 주소에 접근하고, 메모리에 저장된 값을 변경한다는 것이다.

# %rbx = 0x1000, %rcx = 0x200
mov 0x8(%rbx, %rcx), %rax
leq 0x8(%rbx, %rcx), %rax

위의 코드를 보면 mov 0x8(%rbx, %rcx), %rax는 메모리의 0x1208에 저장된 8byte 값을 로드하여 %rax에 저장한다.
하지만, lea 0x8(%rbx, %rcx), %rax는 단순히 사칙연산을 통해 구한 0x1208을 %rax에 저장할 뿐, 메모리에 대한 접근은 일어나지 않는다.

Conditional Jump

Conditional jump란 조건에 따라 jump하는 명령어이다. C언어로 비유하면 if문에 해당한다. 이는 아래와 같은 과정에 따라 수행된다:

  1. 먼저 연산을 수행한다. (sub, cmp, test, and 등)
  2. 그 다음 conditional jump 명령어를 실행한다. (je, jg, jl, jne 등)
    • conditional jump 명령어는 갱신된 flag register 값에 따라 점프할지 말지를 결정한다.

아래는 %rbx < %rcx일 경우 jump 명령을 실행하는 어셈블리 코드이다.

cmp %rbx, %rcx    ; 내부적으로 %rcx - %rbx 수행, 플래그 설정
jg  0x100         ; 결과가 양수면 (%rcx > %rbx), 0x100으로 jump
sub %rbx, %rcx    ; %rcx ← %rcx - %rbx, 플래그 변경됨
jg  0x100         ; 결과가 양수면 jump

또한, 아래는 값이 %rax == 0일 경우 jump 명령을 실행하는 어셈블리 코드이다.

cmp $0, %rax
je  0x100
test %rax, %rax
je   0x100
and %rax, %rax
je   0x100

그리고, 아래는 값이 %rax < 0일 경우 jump 명령을 실행하는 어셈블리 코드이다.

test %rax, %rax
js   0x100
and %rax, %rax
js   0x100

이때 cmp와 sub, and와 test는 어떤 차이를 가지는가? 사실 cmp와 sub, and와 test는 flag register을 완전히 동일하게 바꾼다. 하지만, cmp와 test가 순수하게 file register에만 영향을 주는 반면, sub, and는 destination register에 해당 연산의 결과값을 저장한다.

대표적인 jxx 명령어

Instruction Description
jmp 무조건 jump
je/jz 0이면 jump
jne/jne 0이 아니면 jump
js/jns 음수면/음수가 아니면 jump (sign check)
jg/jge 크면/이상이면 jump (sign comparison)
jl/jle 작으면/이하이면 jump (sign comparison)
ja/jae 크면/이상이면 jump (unsign comparison)
jb/jbe 작으면/이하이면 jump (unsign comparison)

More Conditional Instruction

Conditional Instroctor는 flag register에 그 실행 결과가 결정되는 명령어를 의미한다.

Instruction Description
cmove Move if equal to zero (signed compre)
cmovg Move if greater (signed compre)
... ...
sete Set if equal to zero
setg Set if greater to zero
... ...

Conditional Jump Example

아래와 같은 C 코드는 어셈블리로는 다음과 같이 작성된다.

long absdiff(long x, long y)
{
    long result;
    if (x > y)
        result = x - y;
    else
        result = y - x;
    return result;
}
#No optimization version
absdiff:
    cmp     %rsi, %rdi        # if (x <= y), then...
    jle     0x1000            # jump to 0x1000
    mov     %rdi, %rax
    sub     %rsi, %rax        # rax = x - y
    ret
0x1000:
    mov     %rsi, %rax
    sub     %rdi, %rax        # rax = y - x
    ret
#Optimization version
absdiff:
    mov     %rdi, %rdx         ; rdx = x
    mov     %rsi, %rax         ; rax = y
    sub     %rsi, %rdx         ; rdx = x - y
    sub     %rdi, %rax         ; rax = y - x
    cmp     %rsi, %rdi         ; if (x > y)
    cmovg   %rdx, %rax         ; then rax = rdx (i.e., rax = x - y)
    ret

Loop in Assembly Code

C에서 반복문(while, for)는 if 구문과 goto 함수의 조합으로 구성할 수 있다. 따라서 이를 이용해 반복문을 어셈블리로 나타낼 수 있다. 이는 다음과 같은 단계를 거친다:
1. for 문을 while 문 형식으로 바꾼다. 2. while 문을 아래와 같이 if-goto 형식으로 바꾼다.

while (Test) {
    Body
}
if (!Test)
    goto done;
loop:
    Body
    if (Test)
        goto loop;
done:

3. if-goto 형식을 어셈블리어 문법으로 바꾼다.

Loop in Assembly Code Example

아래와 같은 C 코드는 어셈블리로는 다음과 같이 작성된다.

long sum_1toN(long n) {
    int sum = 0, i = 1;
    while (i <= n) {
        sum += i;
        i++;
    }
    return x;
}
0x000000000040114a <+0>:   mov    $0x0, %edx
0x000000000040114f <+5>:   mov    $0x1, %eax
0x0000000000401154 <+10>:  jmp    0x40115d <sum_1toN+19> #함수 sum_1toN의 시작 주소에서 19 바이트 떨어진 위치
0x0000000000401156 <+12>:  add    %rax, %rdx
0x0000000000401159 <+15>:  add    $0x1, %rax
0x000000000040115d <+19>:  cmp    %rdi, %rax
0x0000000000401160 <+22>:  jle    0x401156 <sum_1toN+12> #함수 sum_1toN의 시작 주소에서 12 바이트 떨어진 위치
0x0000000000401162 <+24>:  mov    %rdx, %rax
0x0000000000401165 <+27>:  ret

Translation of switch Statement

아래와 같은 C코드를 기준으로 설명을 한다.

long switch_ex(long x, long y) {
    long z = y;
    switch (x) {
    case 0:  z = 5; break;
    case 1:  z += 1;   // Fall-through
    case 2:  z -= 2;  break;
    case 4: 
    case 5:  z *= 3;  break;
    default:  z = 1;
    }
    return z;
}

위 코드에는 몇가지 짚고 넘어가야할 점이 있다. 먼저, case1과 case2 사이에는 break 문이 없으므로 fall-through가 존재한다. 또한 case4와 case5는 동일하게 처리되며, case3은 아예 존재하지 않는다. Compiler는 switch 문을 asm으로 번역하기 위해서 Jump table을 생성한다.[3] 위 코드를 figure 2와 같은 jump table을 바탕으로 어셈블리로 번역하면 아래와 같이 된다.

0x401106 <+0>:    cmp    $0x5, %rdi.             #%rdi - 0x5 연산 후 플래그 설정
0x40110a <+4>:    ja     0x401127 <switch_ex+33> #%rdi > 5이면 해당 주소로 jump
0x40110c <+6>:    jmp    *0x402008(,%rdi,8)      #0x402008 주소에 있는 jump table을 기반으로, %rdi * 8 만큼 떨어진 곳의 주소로 jump
0x401113 <+13>:   mov    $0x5, %eax                  #case0
0x401118 <+18>:   ret
0x401119 <+19>:   add    $0x1, %rsi                  #case1
0x40111d <+23>:   lea    -0x2(%rsi), %rax            #case2
0x401121 <+27>:   ret
0x401122 <+28>:   lea    (%rsi,%rsi,2), %rax         #case3
0x401126 <+32>:   ret
0x401127 <+33>:   mov    $0x1, %eax #default case    #case4, 5
0x40112c <+38>:   ret

Function call in Assembly

자세한 내용은 Function call in Assembly 문서를 참조하십시오.

각주

  1. CPU design이 반드시 따라야 하는 abstract한 설계 수준을 보여준다.
  2. 예를 들어 어떤 종류의 연산이 CPU에 의해 지원되는지, CPU에 반드시 존재하는 register의 이름들 등...
  3. 항상 만들지는 않고, jump table을 만드는 것이 효율적으로 보일 때에만 생성한다.