written by yechoi

[어셈블리어] 한큐에 정리하는 어셈블리 기초 본문

Born 2 Code/Assembly

[어셈블리어] 한큐에 정리하는 어셈블리 기초

yechoi 2020. 6. 25. 19:57
반응형

기준 문법: Intel syntax
기준 어셈블러: nasm

 

op-code (명령어)

mov

mov reg, 값 : reg를 값으로 덮어씌움
mov reg, reg : 첫번째 reg를 두번째 reg 값으로 덮어씌움, 단 두 레지스터의 크기가 같아야 한다.

pop

스택으로부터 값을 뽑아냄. pop rax라면 스택 맨 위의 값(rsp가 가리키는 값)을 뽑아다 rax에 집어넣음.

push

오퍼랜드의 내용을 스택에 쌓음

cmp

두개의 오퍼랜드 비교. 비교의 결과는 상태 플래그에 담긴다.
*CF: 부호 없는 숫자의 연산 결과가 비트 범위를 넘어섰을 때 참

cmp 연산 결과 ZF(Zero Flag) CF(Carry Flag)
dest < source 0 1
dest > source 0 0
dest == source 1 0
jmp

특정 위치로 건너 뛰어 코드를 실행.
조건점프명령

call

프로시저 호출. 현재 위치를 스택에 push 하고 jmp한다는 점에서 단순 jmp 와 다르다. call된 위치에서 ret를 실행하면 스택에서 주소를 꺼내와 call한 다음 위치부터 시작

ret

호출된 함수에서 호출한 함수로 복귀



register (64 bits)

연산은 CPU 내 register에서, register는 가장 빠른 저장 공간

cf. 프로그램은 RAM

RAX(64 bits) - EAX (32 bits, Extended AX) - AX(16 bits) - AL(8 bits) - AH(8 bits)

img

데이터 레지스터 (RAX ~ RDX)

RAX(Extended Accumulator Register)

사칙연산 명령어에서 자동으로 사용, 리턴 레지스터
시스템콜의 실질적인 번호를 가리키는 레지스터

RBX(Extended Base Register)

메모리 주소를 저장하는 용도로 사용

RCX(Extended Counter Register)

CPU는 루프 카운터로 ECX를 자동으로 사용

RDX(Extended Data Register)

EAX와 같이 사용됨.

포인터 레지스터(RSI, RDI, RBP, RSP)

RSI(Extended Source Index) / RDI(Extended Destination Index)

문자열 출발지/목적지 주소. 확장 소스 인덱스, 확장 목적지 인덱스 레지스터. 각각 메모리 출발지와 목적지를 나타냄. 고속 메모리 전송 명령어에서 사용.

RSP(Extended Stack Pointer)

현재 스택 주소. 그러니까 스택 맨 윗쪽 주소. 스택에 있는 데이터의 주소를 지정. 계산, 데이터 전송에는 거의 사용되지 않는다

RBP(Extended Base Pointer)

스택 복귀 주소. 고급언어에서 스택에 있는 함수 매개변수와 지역변수를 참조하기 위해서 사용. 고급 수준의 프로그래밍 이외에 일반적 계산과 데이터 전송에서 사용되지 않아야.
RSP, RBP에 대한 자세한 설명

RIP

현재 명령 실행 주소

기타

r8 ~ r15

일반적으로 함수의 매개변수로 사용



메모리

메모리 구조
stack(지역변수)
heap(동적할당)
BSS(uninitialized)
Data(initialized)
Text(code)

'section .bss' 'section .data' 'section .text'과 같은 방식으로 선언한 뒤 변수 선언 또는 코드 작성함

스택 프레임
버퍼
변수
RBP
스택이 시작하는 베이스 포인터
RET
가장 아래쪽에 return adress가 존재해 함수가 끝났을 때 돌아갈 곳을 지정.

메인함수의 스택프레임이 이렇다면, 메인함수가 다른 함수를 불러왔을 때 버퍼 위에 데이터가 차곡차곡 쌓임. 이때 변수가 먼저 쌓이고 RET-RBP-버퍼 순. 해당 함수가 다 실행되면 쌓였던 스택이 사라짐.

스택, rsp, rbp, push, pop의 관계



자료형

allocate memory access memory bytes register C datatype
db byte 1 AL char
dw word 2 AX short
dd dword 4 EAZ int
dq qword 8 RAX long



배열선언

data 영역 (전역변수)
segment .data
arr1 dd 1,2,3,4,5
arr2 dw 1,1,1,1,1,1
arr3 times 5 db 3

arr1은 4바이트 크기의 원소를 5개 가진 배열, 초기값이 1,2,3,4,5
arr2는 2바이트 크기의 원소를 6개 가진 배열, 초기값이 모두 1
arr3은 2바이트 크기의 원소를 8개 가진 배열, 초기값 모두 1

bss 영역 (전역변수)
segment .bss
arr1 resb 20
arr2 resw 30
arr3 resd 40

arr1은 1바이트 크기의 원소 20개를 가진 배열
arr2는 2바이트 크기의 원소 30개를 가진 배열
arr3은 4바이트 크기의 원소 40개를 가진 배열

stack 영역 (지역변수)
void    f(void)
{
    char    c;
    int     i;
    int        j;
    short    s[50];
}
segment .text
_f:
    push ebp
    mov ebp, esp
    sub esp, 112
    ...(생략)

c 1바이트, i 4바이트, j 4바이트, s배열 100바이트 총 109바이트의 스택을 할당해야 한다. 그러나 스택은 4의 배수로 정렬되기 때문에, 112 바이트를 할당한다.

data 영역이나 bss 영역에서 선언된 배열과 달리, 스택 영역에 선언된 배열은 이름이 없다. 따라서 ebp 레지스터를 통해 접근해야 한다.



코드에 적용하면...

변환하고자 하는 c언어 코드

int a[10] = {1,2,3,4,5,6,7,8,9}
int b[10];
int f()
{
    int c[10];

    b[0] = a[1];
    c[2] = a[3];
    return (b[0] + c[2]);
}

어셈블리어 코드

전역변수 할당
segment .data
_a dd 1,2,3,4,5,6,7,8,9
segment .bss
_b resd 10
메인함수 선언
segment .text
_f:
스택프레임 생성
    push rbp
    mov rbp, rsp

이전의 rbp를 스택에 push해 스택에 복귀할 포인터 위치를 저장한다. ebp 를 현재 함수의 스택 주소 구간으로 변경하기 위해서 현재 스택포인터 값을 베이스 포인터 값으로 바꿔준다.

스택에 지역변수 공간 확보
    sub rsp, 40
스택과 레지스터를 활용해 계산
    mov rax, [_a + 4]
    mov [_b], rax
    mov rax, [_a + 12]
    mov [rbp - 32], rax
    mov rax, [-b]
    add rax, [rbp - 32]
스택프레임 회수
    leave

또는

    mov rsp, rbp 
    pop rbp

sub rbp, NUM 으로 할당했던 공간을 회수하고, 저장해둔 함수 실행 전의 rbp를 가져와 원래 rbp 상태로 돌려준다.

종료
    ret



malloc & free

malloc
extern malloc
mov rdi, 8
call malloc

rdi 에 할당할 바이트 수를 입력하고 malloc을 call 한다.

mov QWORD[rax], 3

malloc은 rax로 포인터를 반환한다. 해당 포인터를 활용해, 원하는 값을 집어넣는다.

free
extern free
mov rdi, rax
call free

해제할 포인터를 rdi에 넣고, free를 call한다.

더보기



system call

rax에 따라서 system call로 불러오는 함수가 다르다.
매개변수는 다른 레지스터에서 받음.

다음은 rax가 1와 60일때 system call 예시.

%rax system call %rdi %rsi %rdx
1 sys_write unsigned int fd char *buf size_t count
60 sys_exit int error_code    
section .data
                msg db "Hello World",10,0

section .text
                global_start

_start:
                mov rax, 1
                mov rdi, 1
                mov rsi, msg
                mov rdx, 12
                syscall
                mov rax, 60
                mov rdi, 0
                syscall

rax에 1을 담아 sys_write를 쓰고, 이때 fd(rdi)는 1로 넣어 출력하겠다고 알려준 뒤, msg라는 포인터 변수를 rsi에 넣어줌. 그리고 rax에 1을 담아 sys_exit을 불러오고, 에러코드에 0을 넣어 안전하게 종료.

system call table

주의해야 할 점. macosx나 bsd는 system call 번호를 여러 'class'로 나눠뒀다.

/*
 * Syscall classes for 64-bit system call entry.
 * For 64-bit users, the 32-bit syscall number is partitioned
 * with the high-order bits representing the class and low-order
 * bits being the syscall number within that class.
 * The high-order 32-bits of the 64-bit syscall number are unused.
 * All system classes enter the kernel via the syscall instruction.
 *
 * These are not #ifdef'd for x86-64 because they might be used for
 * 32-bit someday and so the 64-bit comm page in a 32-bit kernel
 * can use them.
 */
#define SYSCALL_CLASS_SHIFT    24
#define SYSCALL_CLASS_MASK    (0xFF << SYSCALL_CLASS_SHIFT)
#define SYSCALL_NUMBER_MASK    (~SYSCALL_CLASS_MASK)

#define SYSCALL_CLASS_NONE    0    /* Invalid */
#define SYSCALL_CLASS_MACH    1    /* Mach */    
#define SYSCALL_CLASS_UNIX    2    /* Unix/BSD */
#define SYSCALL_CLASS_MDEP    3    /* Machine-dependent */
#define SYSCALL_CLASS_DIAG    4    /* Diagnostics */

/* Macros to simpllfy constructing syscall numbers. */
#define SYSCALL_CONSTRUCT_MACH(syscall_number) \
            ((SYSCALL_CLASS_MACH << SYSCALL_CLASS_SHIFT) | \
             (SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
            ((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
             (SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_MDEP(syscall_number) \
            ((SYSCALL_CLASS_MDEP << SYSCALL_CLASS_SHIFT) | \
             (SYSCALL_NUMBER_MASK & (syscall_number)))
#define SYSCALL_CONSTRUCT_DIAG(syscall_number) \
            ((SYSCALL_CLASS_DIAG << SYSCALL_CLASS_SHIFT) | \
             (SYSCALL_NUMBER_MASK & (syscall_number)))

예컨대 read는 syscall_class_unix에 속하기 때문에 upper bits 를 2로 설정한다. 따라서 unix system call 의 system call 은 (0x2000000 + unix syscall #)가 된다. write를 불러오고자 한다면, 다음과 같이 하면 된다.



calling convention(함수호출규약)

함수를 호출하는 방식. 어떻게 리턴하고, 어떻게 지워지는 등을 결정하는 규약.

macos를 포함한 system V AMD64에서는 스택 클린업을 caller가 담당한다.

이때, 스택은 16바이트로 정렬돼야 한다. 함수를 호출할 때 돌아가기 위한 주소 8바이트가 쌓이므로, 함수 호출 전에 미리 8바이트를 쌓아 16바이트 정렬을 해준다.

sub rsp, 8
call _ft_strcpy
add rsp, 8 

이밖에도 push를 이용하면 이처럼 8바이트를 미리 쌓는 효과를 볼 수 있다.

push rdi
call _ft_strlen

rsp를 옮겨주지 않고도 정상 작동한다.



c 소스 어셈블리어변환

gcc -S -fno-stack-protector -mpreferred-stack-boundary=4 -z exectack -o name.a name.c
반응형