본문 바로가기
2️⃣ 개발 지식 B+/OS

어셈블리어 기초

by ddubbu 2024. 10. 2.

[참고자료]


x86 vs x64

  • x86 : 32bit 시스템. Intel 8086 프로세서 이후 80286, 80386 같은 모델들이 등장하면서 유래
  • x64 : 64bit 시스템

 

(32bit) 어셈블리어 명령어

 

헷갈리는 mov 명령어 먼저 이해하기

// C Code
long exchange(long *xp, long y) {
    long x = *xp;
    *xp = y;
    return x;
}

// Assembly code
// xp in %rdi, y in %rsi

exchange:
    movq (%rdi), %rax # Get x at xp. Set as retrun value.
    movq %rsi, (%rdi) # Store y at xp.
    ret # Return.

 

x86 C언어 설명 x86-64
mov a, b 괄호 유무에 따라
의미 상이
[참고 CS:APP 180p] 위 코드 블록 참고

괄호가 있는 경우는 메모리 주소를 의미하고, 없는 경우 레지스터 간의 값 이동을 의미한다.

[예시]

movq a_val, b_val
a_val 값을 b_val 로 복사

movq (a_ptr), b_val
a_ptr이 가리키는 주소에서 값을 읽어와 b_val에 저장함

movq a_val, (b_ptr)
a_val 값을 b_ptr이 가리키는 메모리 주소에 저장함


[예시2] movq %rbp, (%rsp)
b의 값(%rbp)을 a(%rsp가 가리키는 메모리 주소)에 복사한다.
%rbp 값을 스택에 저장함

 
lea a, [b] a = *b b의 주소에 있는 값을 a에 복사 leaq
cmp a, b if문에서 주로 사용 a와 b를 비교  
add a, b a += b a와 b를 더해 a에 결과를 넣기  
sub a, b a -= b    
mul a, b a *= b    
xor a ^= b    
je if (! zero_flag) jump 비교값이 같은 경우 jump  
jne if (zero_flag) jump 비교값이 다른 경우 jump  
call    리턴 주소 (caller 다음 명령어 주소) 스택에 저장
> callee 주소로 이동
> 리턴 주소로 복귀
 
jmp   해당 주소로 점프 / 전으로 돌아갈 수 없음  

 

레지스터 종류

  • CPU 아키텍셔체 따라 레지스터 종류가 다를 수 있음. 범용 레지스터 또한 규약(stdcall)에 따라 다르게 사용될 수도
  • E : extended, R : Register
  • 베이스 레지스터 : 메모리의 특정 영역에 대한 기준 주소 저장, 스택 프레임 관리
x86 x86-64 설명
  rdi
rsi
rdx
rcx
r8
r9
- 범용레지스터
- 함수 호출 시 첫번째 ~ 여섯번째 인수 전달
(6개 이상의 인수를 필요로 할 경우, 추가 인수는 스택을 통해 전달됨
- 함수 인수 전달 외에도 다른 작업에서 사용될 수 있음
  r8 ~ r13 범용 레지스터, 함수 호출 시 인수 전달에 사용
eax rax 함수 리턴값 저장됨
rbx rbx (base register) 종종 배열, 문자열과 같은 구조에 접근하기 위한 기준 포인터
  rbp (base pointer) 함수 호출 스택 프레임의 기준을 설정
esi rsi (범용 레지스터 역할 외) 소스 인수 포인터, 문자열 및 배열 등의 데이터 소스 포인터 역할
edi rdi (범용 레지스터 역할 외)  대상 인수 포인터, 문자열 및 배열의 목적지 포인터 역할
esp rsp top of stack
eip rip (instruction pointer, program counter) 다음에 실행될 명령어 주소

 

; rdi에 첫 번째 인수, rsi에 두 번째 인수를 저장
mov rdi, 5          ; 첫 번째 인수로 5 저장
mov rsi, 10         ; 두 번째 인수로 10 저장
call my_function    ; my_function 호출


; 메모리에서 데이터를 복사하는 경우
mov rsi, source_address ; 원본 주소를 rsi에 저장
mov rdi, destination_address ; 목적지 주소를 rdi에 저장
mov eax, [rsi]          ; 원본 주소에서 값 읽기
mov [rdi], eax          ; 읽은 값을 목적지 주소에 저장

 

 

C 소스파일 ➡️ (GCC 컴파일) ➡️ 어셈블리어

$ gcc -S {file_name}.c  // file_name.s 어셈블리 코드 생성 (컴파일 전처리 후 단계)

// 어셈블리어 주소 포함해서
$ gcc -g -o output_file source_file.c // 디버깅 정보 포함해 컴파일
$ objdump -d output_file // 디스어셈블: binary file to assembly

 

단순 덧셈 연산

int main() {
    int a = 3;
    int b = 4;
    return a + b;
}

 

컴파일 결과

	.file	"test.c" // 파일명 지시자
	.text
	.globl	main
	.type	main, @function
main: // main 함수 시작
.LFB0: // 함수 시작 레이블
	.cfi_startproc // 스택 프레임 시작
	endbr64
	pushq	%rbp // 현재 베이스 포인터를 스택에 푸시
	.cfi_def_cfa_offset 16 // 스택 프레임의 현재 오프셋을 정의
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$3, -8(%rbp) // -8(%rbp) 위치에 정수 3 저장 (첫번째 변수)
	movl	$4, -4(%rbp) // -4(%rbp) 위치에 정수 4 저장 (두번째 변수)
	movl	-8(%rbp), %edx // 첫번째 변수 값을 edx에 로드
	movl	-4(%rbp), %eax // 두번째 변수 값을 edx에 로드
	addl	%edx, %eax // 두 변수의 합을 eax에 저장
	popq	%rbp // 스택에서 이전 베이스 포인터 복원
	.cfi_def_cfa 7, 8
	ret // 함수 종료
	.cfi_endproc // 스택 프레임 처리 종료
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	1f - 0f
	.long	4f - 1f
	.long	5
0:
	.string	"GNU"
1:
	.align 8
	.long	0xc0000002
	.long	3f - 2f
2:
	.long	0x3
3:
	.align 8
4:

 

 

Q. printf 문과 같이 함수 호출이 추가되면?

더보기

 C 소스파일

#include <stdio.h>

int main() {
    int a = 3;
    int b = 4;
    printf("%d\n", a + b);
    return 0;
}

 

gcc -S 결과

	.file	"test.c" // 파일 지시자
	.text
	.section	.rodata
.LC0: // 문자열 리터럴 레이블
	.string	"%d\n"
	.text
	.globl	main
	.type	main, @function
main: // main 함수 시작
.LFB0: // 함수 시작 레이블
	.cfi_startproc // 스택 프레임 시작, 디버깅과 스택 트레이스 지원을 위함 (함수의 매개변수, 지역변수, 리턴 주소 등 정보 저장)
	endbr64
	pushq	%rbp // 현재 베이스 포인터를 스택에 저장
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp // 현재 스택 포인터를 베이스 포인터로 설정
	.cfi_def_cfa_register 6
	subq	$16, %rsp // (로컬 변수용) 스택에서 16byte 공간 확보
	movl	$3, -8(%rbp) // (스택 -8byte 위치) 3 저장 (변수 a)
	movl	$4, -4(%rbp) // (스택 -4byte 위치) 4 저장 (변수 b)
	movl	-8(%rbp), %edx // 변수 a %edx 레지스터에 로드
	movl	-4(%rbp), %eax // 변수 b %eax 레지스터에 로드
	addl	%edx, %eax // 덧셈
	movl	%eax, %esi // 결과를 esi 레지스터에 저장
	leaq	.LC0(%rip), %rax // 문자열 리터럴 주소를 rax 레지스터에 로드
	movq	%rax, %rdi // rdi에 문자열 주소 저장
	movl	$0, %eax
	call	printf@PLT // printf 함수 호출
	movl	$0, %eax // 함수 반환값 0으로 설정
	leave
	.cfi_def_cfa 7, 8
	ret // 함수 종료, 호출한 곳으로 복귀
	.cfi_endproc // 스택 프레임 종료
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8 // 8byte 정렬
	.long	1f - 0f
	.long	4f - 1f
	.long	5
0:
	.string	"GNU"
1:
	.align 8
	.long	0xc0000002
	.long	3f - 2f
2:
	.long	0x3
3:
	.align 8
4:

 

 

gcc -O2 -S 결과 (최적화 후 주요 코드만 남김)

.LC0:
	.string	"%d\n"

main
    movl    $7, %edx      // 덧셈 결과인 7을 edx에 저장
    movl    $1, %edi      // %d 형식 문자열의 주소를 edi에 저장
    leaq    .LC0(%rip), %rsi // %LC0 문자열 리터럴 주소를 rsi에 저장
    call    __printf_chk@PLT // printf 호출

 

 

 

 

Q. 레지스터 개수가 한정되었는데 어떻게 복잡한 연산을 진행하는지 궁금해서 여러 값을 넣어보았다.

더보기

C언어

#include <stdio.h>

int main() {
    int a = 1 + 3 + 3 + 3 + 3;
    printf("%d %d %s %s", a, 123, "ABC", "HELLO");
    return 0;
}

 

어셈블리어 (생략 버전)

section .data
    fmt db "%d %d %s %s", 0      ; 포맷 문자열 정의
    str1 db "ABC", 0              ; 문자열 리터럴
    str2 db "HELLO", 0            ; 문자열 리터럴

section .text
    global main
    extern printf

main:
    ; a = 1 + 3 + 3 + 3 + 3; 계산
    mov eax, 1                    ; eax에 1 저장
    add eax, 3                    ; eax에 3 추가
    add eax, 3                    ; eax에 3 추가
    add eax, 3                    ; eax에 3 추가
    add eax, 3                    ; eax에 3 추가
    ; eax = 13이 됨

    ; printf 호출
    push str2                     ; "HELLO" 문자열 주소 푸시
    push str1                     ; "ABC" 문자열 주소 푸시
    push 123                      ; 정수 123 푸시
    push eax                      ; 계산된 a (13) 푸시
    push fmt                      ; 포맷 문자열 주소 푸시
    call printf                   ; printf 호출
    add esp, 20                  ; 스택 정리 (푸시한 인자의 크기만큼)

    ; 프로그램 종료
    mov eax, 0                    ; 종료 코드 0
    ret