본문 바로가기
Computer Science/language

[assembly] Assembly 함수, 시스템 콜 정리

by yeong-yi 2025. 5. 25.
Contents

함수

  • 어셈블리어에서 프로그램이 처리해야 할 명령어들을 한 덩어리로 모아 놓은 코드블록
  • C나 파이썬 같은 고급 언어에서 함수 정의하는 것처럼, 어셈블리어에서도 라벨(Label)로 특정 구역을 표시하고 명령어를 활용해 함수 사용
  • 함수는 서브루틴, 프로시저 , 함수라는 용어로 혼용
  • 함수를 호출한 함수는 call 명령어로 함수를 불러 사용, 호출된 함수는 ret 명령어를 이용해 다시 이전 함수에서 실행중이던 코드로 돌아감.

스택

  • 자료구조의 한 종류, LIFO방식으로 동작하는 구조
  • x86 아키텍처의 스택은 높은 메모리 주소에서 낮은 메모리 주소로 자라나는 특징.
  • → 데이터를 추가할 때마다 메모리 주소가 감소하며, 데이터를 제거하면 다시 증가하는 특성

rsp: 스택 포인터 레지스터

→ 항상 스택의 가장 위(Top)을 가리키며, push 명령어를 실행하면 rsp가 감소하면서 스택에 값이 저장. 반대로 pop을 실행하면 rsp가 증가하면서 스택에 저장되어 있던 값을 꺼냄


push

스택에 값 저장 명령어. push 실행하면 rsp가 감소하면서 스택에 값 저장

형태: push val -값 val을 스택에 저장함

-> 내부적으로 수행되는 연산

rsp -= 8

[rsp] = val

[실행 전 Register]
rsp = 0x7fffffffc400
[Stack]
0x7fffffffc400 | 0x0 ← rsp
0x7fffffffc408 | 0x0
[Code]
push 0x31337
[실행 후 Register]
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc400 | 0x31337 <- rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0

pop

stack이랑 반대. 값을 꺼내는 명령어이며 pop을 실행하면 rsp가 증가하면서 스택에 저장되어 있던 값 꺼냄

[실행 전 Register]
rax = 0
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp 
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0
[Code]
pop rax ;stack으로 값이 빠져나와 rax 주소에 저장
[실행 후 Register]
rax = 0x31337
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc400 | 0x0 <- rsp 
0x7fffffffc408 | 0x0

함수를 부르는 행위를 호출(Call)이라고 부르며, 돌아오는 것을 반환(Return)이라고 부른다.

call 다음의 명령어 주소(Return Address, 반환주소)를 스택에 저장하고 함수로 rip를 이동시킨다.

call

함수 호출시 사용

[실행 전 Register]
rip = 0x400000
rsp = 0x7fffffffc400 

[Stack]
0x7fffffffc3f8 | 0x0
0x7fffffffc400 | 0x0 <- rsp

[Code]
0x400000 | call 0x401000  <- rip
0x400005 | mov esi, eax
...
0x401000 | push rbp
[실행 후 Register]
rip = 0x401000
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005  <- rsp
0x7fffffffc400 | 0x0

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | push rbp  <- rip

call 0x401000 실행하고 나면, 스택 포인터인 rsp 위치에 반환 주소인 0x400005가 저장되어 있고, rip 레지스터가 0x401000으로 바뀐 모습 확인

leave

프로지서가 반환되기 전, 스택 프레임(Stack Frame)정리하는 명령어

Stack Frame이란?

  • 스택은 함수별로 자신의 지역변수 또는 연산과정에서 부차적으로 생겨나는 임시 값들을 저장하는 영역
  • 만약 이 스택 영역을 아무런 구분 없이 사용하게 된다면 서로 다른 함수가 같은 메모리 영역을 사용하게 됨
  • 함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 스택 프레임 단위 사용
  • 대부분의 ABI(Application binary interface)에서는 함수는 호출될 때 자신만의 스택 프레임을 만들고 반환할 때 이를 정리
[실행 전 Register]
rsp = 0x7fffffffc400
rbp = 0x7fffffffc480

[Stack]
0x7fffffffc400 | 0x0 <- rsp
...
0x7fffffffc480 | 0x7fffffffc500 <- rbp
0x7fffffffc488 | 0x31337 

[Code]
leave
[실행 후 Register]
rsp = 0x7fffffffc488
rbp = 0x7fffffffc500

[Stack]
0x7fffffffc400 | 0x0
...
0x7fffffffc480 | 0x7fffffffc500
0x7fffffffc488 | 0x31337 <- rsp
...
0x7fffffffc500 | 0x7fffffffc550 <- rbp

ret

함수에서 반환해 원래 실행하던 코드로 돌아오는 명령어

[실행 전 Register]
rip = 0x401008
rsp = 0x7fffffffc3f8

[Stack]
0x7fffffffc3f8 | 0x400005    <- rsp
0x7fffffffc400 | 0

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | mov rbp, rsp  
...
0x401007 | leave
0x401008 | ret  <- rip
[실행 후 Register]
rip = 0x400005
rsp = 0x7fffffffc400

[Stack]
0x7fffffffc3f8 | 0x400005
0x7fffffffc400 | 0x0    <- rsp

[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax   <- rip
...
0x401000 | mov rbp, rsp  
...
0x401007 | leave
0x401008 | ret

ret을 실행한 후 rip가 반환 주소인 0x400005로 돌아감


어셈블리어에서의 함수 선언

  1. 함수 시작 위치를 나타낼 라벨 정의
  2. 스택 프레임이 필요한 경우, 함수 프롤로그를 통해 스택 프레임 구성
  3. 함수 내부에서 실제 동작 구현
  4. 함수 마지막에 함수 에필로그를 통해 스택프레임을 해제하고, ret 명령어로 종료
<x86>
add: ;add라는 라벨 정의
    push ebp ;함수 프롤로그 수행
    mov ebp, esp
    mov eax, [ebp + 8] ;인자 전달
    add eax, [ebp + 12]
    leave ;함수 에필로그 스택 프레임 정리
    ret

 

<x64>
add:
    push rbp ;함수 프롤로그 수행
    mov rbp, rsp
    mov rax, rdi ;인자 전달
    add rax, rsi
    pop rbp ;함수 에필로그 스택프레임 정리
    ret

시스템 콜: Opcode

현재 운영체제는 크게 사용자 모드(User Mode)커널 모드(Kernel Mode)로 나뉘어 동작

→ 사용자가 권한 없이 운영체제 내부의 데이터를 읽거나 쓸 수 없게 하기 위해

 

시스템 콜

  • System Call, syscall은 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용
  • 예를들어 사용자가 cat flag 라는 리눅스 명령어를 실행하면, flag라는 파일을 읽어 사용자의 터미널화면에 출력해주어야 한다.
  • 그런데 flag는 파일 시스템에 존재하므로 이를 읽으려면 접근을 해야 하는데 유저 모드에선 직접 접근할 수 없기에 커널이 도움을 줘야 한다.
  • 여기서 도움이 필요하다는 요청을 시스템 콜이라고 한다.
  • C언어의 read(), wirte()는 내부적으로 read, write 시스템 콜을 사용

x86 시스템 콜 사용

x86 모드와 x64 모드에서 시스템 콜을 사용하는 방법이 조금씩 다르기 때문에 나누어 설명해야 한다.

 

x86

<int 0x80>
요청하는 시스템 콜 번호: eax 
인자 순서: ebx -> ecx -> edx -> esi -> edi -> ebp -> ...

 

x86에서는 int 0x80 명령어를 사용해 시스템콜을 호출할 수 있다.

이때 eax 레지스터에 호출하고자 하는 시스템 콜의 번호를 넣는다.

시스템 콜 번호란?
운영체제에서 제공하는 각 기능에 고유하게 할당된 값
예를 들어 파일을 열기 위한 open 시스템 콜은 특정 번호(ex. 5번)가 지정되어 있어 eax에 이 번호를 저장하면 커널이 어떤 작업을 수행할지 알 수 있게 된다.
그리고 나머지 이자들은 ebx, ecx, edx, esi, edi, ebp 등 순서대로 전달된다.
그 후 시스템 콜의 반환값은 eax 레지스터에 저장된다.

 

open 시스템 콜을 호출한다고 가정하자.

이 때 open 시스템 콜을 호출하기 위한 번호는 5 이기 때문에 eax 레지스터에 5를 저장한다.

ebx에는 파일 이름의 주소, ecx에는 열기 모드, edx에는 추가 옵션이 저장된다. 

아래 코드는 시스템 콜을 사용해 "sample.txt" 파일을 여는 예시다.

 

x64

<syscall>
요청하는 시스템 콜 번호: rax
인자 순서: rdi -> rsi -> rdx -> rcs -> r8 -> r9 -> 스택

 

x64에선 syscall 명령어를 사용해 시스템 콜을 호출할 수 있다.

rax 레지스터에 호출하고자 하는 시스템 콜의 번호를 넣고, 나머지 인자들은 rdi, rsi, rdx, rcx, r8, r9 스택 순서대로 전달된다.

그 후 시스템 콜의 반환 값은 rax에 저장된다.

위 예시에서 수행하는 동작을 코드로 변환 시켜 보자.

section .data
    filename db "sample.txt", 0
    buffer   times 100 db 0

section .text
    global _start

_start:
    ; 파일 열기: open("sample.txt", O_RDONLY, 0)
    mov rax, 2           
    lea rdi, [filename]
    mov rsi, 0           
    xor rdx, rdx      
    syscall

 

x64 시스템 콜 테이블

시스템 콜 테이블의 개수는 총 300개에 달하고, 구글에 적절히 검색하면 쉽게 찾을 수 있기에 외울 필요는 없다. 중요한 몇 가지 시스템 콜만 표로 정리했다.

syscall rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
read 0x00 unsigned int fd char *buf size_t count
write 0x01 unsigned int fd const char *buf size_t count
open 0x02 const char *filename int flags umode_t mode
close 0x03 unsigned int fd    
mprotect 0x0a unsigned long start size_t len unsigned long prot
connect 0x2a ins sockfd struct sockaddr * addr int addrlen