Posts [CS] 프로세스 메모리 구조(Memory Model), 그리고 생애 첫 어셈블리어(Assembly Language)
Post
Cancel

[CS] 프로세스 메모리 구조(Memory Model), 그리고 생애 첫 어셈블리어(Assembly Language)

Register 가 이런거구나… 라고 생각할무렵 동료(펭도리)로부터 동빈나 님의 유튭 영상을 하나 전해받았다.

레지스터의 용도와 시스템 콜 이해하기

나도 프로세스 메모리 구조에 대한 조금 더 구체적이고 명시적인 설명이 필요했기 때문에 바로 열었다.
영상을 보니 대충 무슨 말인지 알겠는데 앞 영상을 안봐서 시원치가 않다.
그래서 재생목록을 쭈욱봤더니 칼리 리눅스 (Kali Linux) 로 실습 환경을 구축하고, 실습 중이었다.
나에겐 얼마전 VM 에 설치하고 예뻐서 그대로 둔 ubuntu 가 있다.
나도 바로 실습에 참여했다.


프로세스 메모리 구조(Memory Model) 찾아왔다가 어셈블리어(Assembly Language) 만난 썰


1. 어셈블리어(Assembly Language)로 Hello World 출력하기



linuxnano 라는 vim 과 같은 편집기가 있다는 걸 얼마전에 알았고,
장기적으로 좋을 것 같아서 되도록이면 vim 을 사용해보려고 노력하고 있지만
순수 linux 에서 vim 은 방향키 조차 인정하지 않는걸 경험한터라 일단 nano 로 해봤다…..

shell 도 쓰던게 편하니까 배웠던 sshubuntu 에 접속해서 작업해봤다.
(그런데 또 지금 생각해보니 WSL zsh 로 접속했으니 vi 를 썼어도 상관없었을 것 같긴 하다.)


  1. helloworld.s 파일 생성

    1
    
    $ nano helloworld.s
    
  2. 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 + XY 로 저장 Enter 로 편집기 종료
  3. 작성한 코드 확인
    1
    
    $ cat hellowrold.s
    
  4. Assembly Language 로 작성한 코드를 목적 코드로 컴파일

    1
    
     $ nasm -f elf64 -o helloworld.o helloworld.s
    
    • nasm 이 설치가 안되서 root 권한으로 다시 접속해서 진행
    • sudo apt-get install nasm
  5. 실제로 실행해 볼 수 있는 프로그램으로 컴파일

    1
    
    $ ld -o helloworld helloworld.o
    
    • ld 명령어가 없어서 binutils 설치 후 진행
    • sudo apt install binutils

    screenshot010

    • 실행할 수 있는 파일 helloworld 가 생겼다.
    • linux 에서는 실행할 수 있는 파일은 기본적으로 green 으로 표시된다고 한다.
  6. 프로그램 실행

    1
    
    $ ./helloworld
    

    screenshot011

    • 성공! 너무너무너무 신기하다.
    • 내가 Register 에게 직접 명령을 한건가? 그냥 어셈블리어로 코딩했다고 표현하는게 맞겠지?
    • 이제 다음은 이 Register 가 어떤 방식으로 사용되는지 볼 차례다.


2. 레지스터의 용도와 시스템 콜 이해하기


screenshot012

x64 Architecture


  1. 각 레지스터의 용도

블로그에서 봤던대로 64bit는 R 로 시작하고 32bit는 E 로 시작한다.
그리고 조금전 사용했던 rax, rdi 등등의 register 들이 보인다.


- 이런 register 들은 각각의 쓰임새가 정해져 있다.

RegisterRole
raxsystem call 의 실질적인 번호를 가리키고, 함수 실행 후 결과가 담긴다. 핵심적인 역할.
rbxbase register. 메모리 주소를 지정.
rcxcounter register. 주로 반복문에 사용.
rdxdata register. 연산 실행시 rax 와 함께 많이 사용함


- rsi, rdi, rbp, rsp : pointer register. 특정한 주소를 가리킴

RegisterRole
rsi메모리 이동, 비교시 출발 주소를 가리킴
rdi메모리 이동, 비교시 목적지 주소를 가리킴
rbp함수의 파라미터나 주소를 가리킬때
rsp스택에서 삽입, 삭제 후 가장 위에 있는 주소를 가리키는 중요한 레지스터


  • 하나하나 다 알아야 할 필요는 없고, 이렇게 존재하고, 쓰임새가 다르다는 것 정만 알면된다.
  • 대게는 함수의 매개변수로 많이 사용이 된다.


  1. 시스템 콜 이해하기

    LINUX SYSTEM CALL TABLE FOR X86 64

%raxSystem call%rdi%rsi%rdx%r10%r8%r9
0sys_readunsigned int fdchar *bufsize_t count   
1sys_writeunsigned int fdconst char *bufsize_t count   
2sys_openconst char *filenameint flagsint mode   
3sys_closeunsigned int fd     
4sys_statconst char *filenamestruct stat *statbuf    
5sys_fstatunsigned int fdstruct stat *statbuf    
6sys_lstatfconst char *filenamestruct stat *statbuf    
7sys_pollstruct poll_fd *ufdsunsigned int nfdslong timeout_msecs   
8sys_lseekunsigned int fdoff_t offsetunsigned int origin   
9sys_mmapunsigned long addrunsigned long lenunsigned long protunsigned long flagsunsigned long fdunsigned long off
10sys_mprotectunsigned long startsize_t lenunsigned long prot   


  • table 이 더 있지만 위에서 썼던 register 가 대부분 위쪽에 이써서 10번까지만 가져왔다.
  • raxSystem 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를 정의
  • rax1 을 넣어 Systme write 라는 System call 을 불러옴
  • rdi 즉, file disk1 을 넣어 문자를 출력하겠다고 System call 함수의 매개변수로 넣음
  • 정확히 어떠한 문자를 출력할건지 포인터 변수 msgrsi 의 값에 넣어줌
  • rdx 에 문자열을 충분할 수 있는 충분한 길이 12 를 넣어줌
  • System call 을 불러옴
  • rax 60sys_exit 으로 프로그램 종료이다.
  • rdi 는 에러코드인데 0 을 넣어 안전하게 종료한다.


3. 프로세스 메모리 구조(Memory Model)


screenshot013

  • 이 그림은 0부터 2 ** 32 - 1 까지 사용할 수 있는 32bit 운영체제 메모리다.
  • 현재 많이 사용되는 64비트도 메모리 구조는 거의 비슷하다고 한다.
  • 실제로 Process 가 실행되면 메인 메모리 중 하나의 segment 는 이러현 메모리 구조를 갖는다고 한다.

    • 드디어 알고 싶었던 부분에 조금 근접한 부분이 나온 것 같다.
  • 그리고 이제서야 저 그림이 좀 이해가 가는게 위에서 어셈블리어로 코딩했던 내용이
    • Text(Code) 영역
    • Data 영역
    • Heap 영역
    • Stack 영역
  • 으로 나뉘어진게 그림과 매치가 된다.


MemoryContents
Stack함수, 지역변수
  
  
  
Heap동적 malloc()
BSS(uninitialized)아직 초기화되지 않은 변수
Data(initialized)초기화된 변수
Text(Code)실제 소스 코드


  • 이제 컴퓨터가 알아들을 수 있는 기계어가 되었고,
  • 프로그램이 실행할 때, 텍스트에 있는 어셈블리 코드들이 한줄 한줄 읽히며 동작한다.


4.스택 프레임(Stack Frame) 이해하기



시스템 해킹이 뭔지도 모르겠고, 원래 내가 학습하려했던 부분은 아니지만 스택 프레임이 뭔지 궁금해졌으니 하나만 더 들어봐야겠다.
이번에는 C 를 이용해서 코드를 작성한다.

  1. 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;
}

-

  1. Assembly Language 로 컴파일

    1
    
    $ gcc -S -fno-stack-protector -mpreferred-stack-boundary=4 -z execstack -o sum.a sum.c
    
    • gcc install
    • stack 의 취약점을 보완하기 위해 만든 protector 해제
    • 64비트 운영체제 언어로 컴파일
  2. 작성한 코드 확인

    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언어도 처음 써봤다.


  1. 스택프레임의 원리

    • main() 함수 호출
       
    5버퍼buffer
    4변수 csum(1, 2)
    3RBPbase point
    2RETreturn address
    1main()stack frame


    1. main() 함수가 불려지면
    2. RET 생성, 특정한 함수가 끝나고 돌아갈 곳, 사실 startmain() 을 불러왔기 때문에 함수가 끝나면 자연스럽게 RET 로 돌아감. 모든 함수는 이렇게 return address 를 갖는다.
    3. RBP 스택이 시작하는 포인트
    4. 변수 c = sum(1, 2)
    5. 버퍼


    • sum() 함수 호출
        
    9버퍼buffer여기까지 sum()
    8RBPbase point 
    7RETreturn address 
    6변수 y2 
    5변수 x1 
    4버퍼buffer여기까지 main()
    3변수 csum(1, 2) 
    2RBPbase point 
    1sum()stack frame 


    1. sum() 함수가 불려지면
    2. RBP 생성
    3. 변수 c = sum(1, 2)
    4. 버퍼
    5. 변수 x = 1
    6. 변수 y = 2
    7. sum() 이 호출됐기 때문에 RET 생성
    8. RBP 스택
    9. 버퍼
    10. sum() 이 다 실행되고 나면 sum() 의 결과 값 3변수 c 에 담김
    11. main() 함수는 최종적으로 변수 c 에 담긴 3return


5. 이게 다 머선 129…



  • 얼떨떨하지만 일단 기계어로 뭔가가 됐다는게 기분이 좋다.
  • call by referencecall by value 를 공부할 때, C 로 된 영상을 봐서 겨우 이해가 갔었는데, 직접 C 로 작성하고 컴파일도 해보니 신기했다.
  • 내가 오늘 알아야 했던 건 이것보다 윗단의 이야기인 것 같은데 process, thread … 에게 다시 돌아가 봐야겠다.
  • 그런데 그래서 Node.js 8기통 엔진은 C++ 로 만들어졌다고 알고있는데, 그렇다면 내가 작성한 JS 코드는 얘가 컴파일해서 오늘 했던 과정처럼 실행을 시켜주는건가.


참조

This post is licensed under CC BY 4.0 by the author.

[CS] Process와 Thread를 파다보니 엉뚱하게 Register에 도착해있다.

[CS] 드디어 Process & Thread 시뮬레이션 해보기

Comments powered by Disqus.