Register
가 이런거구나… 라고 생각할무렵 동료(펭도리
)로부터 동빈나
님의 유튭 영상을 하나 전해받았다.
나도 프로세스 메모리 구조에 대한 조금 더 구체적이고 명시적인 설명이 필요했기 때문에 바로 열었다.
영상을 보니 대충 무슨 말인지 알겠는데 앞 영상을 안봐서 시원치가 않다.
그래서 재생목록을 쭈욱봤더니 칼리 리눅스
(Kali Linux)
로 실습 환경을 구축하고, 실습 중이었다.
나에겐 얼마전 VM
에 설치하고 예뻐서 그대로 둔 ubuntu
가 있다.
나도 바로 실습에 참여했다.
프로세스 메모리 구조(Memory Model) 찾아왔다가 어셈블리어(Assembly Language) 만난 썰
1. 어셈블리어(Assembly Language)로 Hello World 출력하기
linux
에 nano
라는 vim
과 같은 편집기가 있다는 걸 얼마전에 알았고,
장기적으로 좋을 것 같아서 되도록이면 vim
을 사용해보려고 노력하고 있지만
순수 linux
에서 vim
은 방향키 조차 인정하지 않는걸 경험한터라 일단 nano
로 해봤다…..
shell
도 쓰던게 편하니까 배웠던 ssh
로 ubuntu
에 접속해서 작업해봤다.
(그런데 또 지금 생각해보니 WSL zsh
로 접속했으니 vi
를 썼어도 상관없었을 것 같긴 하다.)
helloworld.s 파일 생성
1
$ nano helloworld.s
Assembly Language 작성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
section .data msg db "Hello World" 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
Ctrl + X
후Y
로 저장Enter
로 편집기 종료
- 작성한 코드 확인
1
$ cat hellowrold.s
Assembly Language 로 작성한 코드를 목적 코드로 컴파일
1
$ nasm -f elf64 -o helloworld.o helloworld.s
nasm
이 설치가 안되서root
권한으로 다시 접속해서 진행sudo apt-get install nasm
실제로 실행해 볼 수 있는 프로그램으로 컴파일
1
$ ld -o helloworld helloworld.o
ld
명령어가 없어서binutils
설치 후 진행sudo apt install binutils
- 실행할 수 있는 파일
helloworld
가 생겼다. linux
에서는 실행할 수 있는 파일은 기본적으로green
으로 표시된다고 한다.
프로그램 실행
1
$ ./helloworld
- 성공! 너무너무너무 신기하다.
- 내가
Register
에게 직접 명령을 한건가? 그냥 어셈블리어로 코딩했다고 표현하는게 맞겠지? - 이제 다음은 이
Register
가 어떤 방식으로 사용되는지 볼 차례다.
2. 레지스터의 용도와 시스템 콜 이해하기
- 각 레지스터의 용도
블로그에서 봤던대로 64bit는 R
로 시작하고 32bit는 E
로 시작한다.
그리고 조금전 사용했던 rax
, rdi
등등의 register
들이 보인다.
- 이런 register
들은 각각의 쓰임새가 정해져 있다.
Register | Role |
---|---|
rax | system call 의 실질적인 번호를 가리키고, 함수 실행 후 결과가 담긴다. 핵심적인 역할. |
rbx | base register . 메모리 주소를 지정. |
rcx | counter register . 주로 반복문에 사용. |
rdx | data register . 연산 실행시 rax 와 함께 많이 사용함 |
- rsi
, rdi
, rbp
, rsp
: pointer register
. 특정한 주소를 가리킴
Register | Role |
---|---|
rsi | 메모리 이동, 비교시 출발 주소를 가리킴 |
rdi | 메모리 이동, 비교시 목적지 주소를 가리킴 |
rbp | 함수의 파라미터나 주소를 가리킬때 |
rsp | 스택에서 삽입, 삭제 후 가장 위에 있는 주소를 가리키는 중요한 레지스터 |
- 하나하나 다 알아야 할 필요는 없고, 이렇게 존재하고, 쓰임새가 다르다는 것 정만 알면된다.
- 대게는 함수의 매개변수로 많이 사용이 된다.
시스템 콜 이해하기
%rax | System call | %rdi | %rsi | %rdx | %r10 | %r8 | %r9 |
---|---|---|---|---|---|---|---|
0 | sys_read | unsigned int fd | char *buf | size_t count | |||
1 | sys_write | unsigned int fd | const char *buf | size_t count | |||
2 | sys_open | const char *filename | int flags | int mode | |||
3 | sys_close | unsigned int fd | |||||
4 | sys_stat | const char *filename | struct stat *statbuf | ||||
5 | sys_fstat | unsigned int fd | struct stat *statbuf | ||||
6 | sys_lstat | fconst char *filename | struct stat *statbuf | ||||
7 | sys_poll | struct poll_fd *ufds | unsigned int nfds | long timeout_msecs | |||
8 | sys_lseek | unsigned int fd | off_t offset | unsigned int origin | |||
9 | sys_mmap | unsigned long addr | unsigned long len | unsigned long prot | unsigned long flags | unsigned long fd | unsigned long off |
10 | sys_mprotect | unsigned long start | size_t len | unsigned long prot |
table
이 더 있지만 위에서 썼던register
가 대부분 위쪽에 이써서 10번까지만 가져왔다.rax
가System call
을 불러와서 매개변수를 사용하게 된다.System call
함수는Kernel
단에 마련되어 있다.- 표에 따라 아까 작성했던 코드를 읽어주신다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
section .data
msg db "Hello World"
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
- data 영역에 “Hello World” 라는 문자열을 만든다.
- 그 문자열의 위치를 가리키는 포인터 msg를 변수로 선언
- text code 영역에 가장 먼저 실행되는 첫번째 함수 start를 정의
rax
에1
을 넣어Systme write
라는System call
을 불러옴rdi
즉,file disk
에1
을 넣어 문자를 출력하겠다고System call
함수의 매개변수로 넣음- 정확히 어떠한 문자를 출력할건지 포인터 변수
msg
를rsi
의 값에 넣어줌 rdx
에 문자열을 충분할 수 있는 충분한 길이12
를 넣어줌System call
을 불러옴rax
60
은sys_exit
으로 프로그램 종료이다.rdi
는 에러코드인데0
을 넣어 안전하게 종료한다.
3. 프로세스 메모리 구조(Memory Model)
- 이 그림은 0부터 2 ** 32 - 1 까지 사용할 수 있는 32bit 운영체제 메모리다.
- 현재 많이 사용되는 64비트도 메모리 구조는 거의 비슷하다고 한다.
실제로
Process
가 실행되면 메인 메모리 중 하나의 segment 는 이러현 메모리 구조를 갖는다고 한다.- 드디어 알고 싶었던 부분에 조금 근접한 부분이 나온 것 같다.
- 그리고 이제서야 저 그림이 좀 이해가 가는게 위에서 어셈블리어로 코딩했던 내용이
- Text(Code) 영역
- Data 영역
- Heap 영역
- Stack 영역
- 으로 나뉘어진게 그림과 매치가 된다.
Memory | Contents |
---|---|
Stack | 함수, 지역변수 |
Heap | 동적 malloc() |
BSS(uninitialized) | 아직 초기화되지 않은 변수 |
Data(initialized) | 초기화된 변수 |
Text(Code) | 실제 소스 코드 |
- 이제 컴퓨터가 알아들을 수 있는 기계어가 되었고,
- 프로그램이 실행할 때, 텍스트에 있는 어셈블리 코드들이 한줄 한줄 읽히며 동작한다.
4.스택 프레임(Stack Frame) 이해하기
시스템 해킹이 뭔지도 모르겠고, 원래 내가 학습하려했던 부분은 아니지만 스택 프레임
이 뭔지 궁금해졌으니 하나만 더 들어봐야겠다.
이번에는 C
를 이용해서 코드를 작성한다.
- C언어로 코드 작성
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
int main(void) {
int c = sum(1, 2);
return c;
}
-
Assembly Language 로 컴파일
1
$ gcc -S -fno-stack-protector -mpreferred-stack-boundary=4 -z execstack -o sum.a sum.c
gcc install
stack
의 취약점을 보완하기 위해 만든protector
해제64비트
운영체제 언어로 컴파일
작성한 코드 확인
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
.file "sum.c" .text .globl sum .type sum, @function sum: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %edx movl -8(%rbp), %eax addl %edx, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size sum, .-sum .globl main .type main, @function main: .LFB1: .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 movl $2, %esi movl $1, %edi call sum movl %eax, -4(%rbp) movl -4(%rbp), %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size main, .-main .ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.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:
- 진짜 어셈블리어로 바꼈다.
- C언어도 처음 써봤다.
스택프레임의 원리
- main() 함수 호출
5 버퍼 buffer 4 변수 c sum(1, 2) 3 RBP base point 2 RET return address 1 main() stack frame main()
함수가 불려지면RET
생성, 특정한 함수가 끝나고 돌아갈 곳, 사실start
가main()
을 불러왔기 때문에 함수가 끝나면 자연스럽게RET
로 돌아감. 모든 함수는 이렇게return address
를 갖는다.RBP
스택이 시작하는 포인트변수 c
= sum(1, 2)- 버퍼
- sum() 함수 호출
9 버퍼 buffer 여기까지 sum() 8 RBP base point 7 RET return address 6 변수 y 2 5 변수 x 1 4 버퍼 buffer 여기까지 main() 3 변수 c sum(1, 2) 2 RBP base point 1 sum() stack frame sum()
함수가 불려지면RBP
생성변수 c
= sum(1, 2)- 버퍼
변수 x
= 1변수 y
= 2sum()
이 호출됐기 때문에RET
생성RBP
스택- 버퍼
sum()
이 다 실행되고 나면sum()
의 결과 값3
이변수 c
에 담김main()
함수는 최종적으로변수 c
에 담긴3
을return
함
5. 이게 다 머선 129…
- 얼떨떨하지만 일단 기계어로 뭔가가 됐다는게 기분이 좋다.
call by reference
와call by value
를 공부할 때, C 로 된 영상을 봐서 겨우 이해가 갔었는데, 직접 C 로 작성하고 컴파일도 해보니 신기했다.- 내가 오늘 알아야 했던 건 이것보다 윗단의 이야기인 것 같은데
process
,thread
… 에게 다시 돌아가 봐야겠다. - 그런데 그래서
Node.js
8기통 엔진은C++
로 만들어졌다고 알고있는데, 그렇다면 내가 작성한JS
코드는 얘가 컴파일해서 오늘 했던 과정처럼 실행을 시켜주는건가.