Linux Kernel

이동화·2025년 7월 10일

LTS vs Stable

LTS : 장기간의 지원을 제공하는 커널 버전으로, 일정 기간동안 보안 업데이트, 버그 수정 등의 패치가 지속적으로 제공되어 안정적인 환경을 유지해줌.

Stable : 새로운 기능을 빠르게 추가해주는 커널로, 최신 하드웨어 및 네트워크 기능이 필요한 시스템에서 사용한다. 지원 기간은 LTS보단 조금 짧다.

커널 프로젝트는 각 버전별로 stable branch를, 그 중 특정 버전을 LTS로 지정하여 장기 지원을 별행한다.


Kernel space

Kernel : 유저와 하드웨어가 통신을 할 때 중간 다리에서 computing 되어 있는 대로 실행해주는 프로그램

Kernel space
Linux에서는 메모리를 커널 영역유저 영역으로 나누어 관리하는데, 두 영역을 구분함으로써 프로세스가 컴퓨터 자원을 무한정으로 사용할 수 없고, 프로세스가 다른 프로세스의 메모리를 침범하지 않도록 할 수 있어 시스템의 안정성과 보안을 유지할 수 있다. 따라서 유저 영역에서 컴퓨터 자원을 사용하혀면 커널에 syscall을 통해 요청을 보내야 한다.

syscall이 호출된 후 CPU는 kernel mode로 전환되고, entry_SYSCALL_64() 함수에 진입하여 유저가 요청한 시스템 콜을 sys_call_table에서 찾아 호출하게 된다.

Linux Namespace : linux 기능 중 하나로, 운영 체제 수준에서 리소스의 격리를 제공하는 기능이다. 하나의 시스템에서 실행되는 프로세스 그룹의 리소스를 분리하여 docker처럼 독립적인 서버인 것처럼 동작하게 한다. 그러나 프로세스마다 다른 namespace를 가지는 것은 아니며, 리소스를 공유하는 프로세스마다 하나의 namespace를 공유하게 된다.

Task

응용 프로그램 입장에서는 thread와 process는 구분할 대상이다. thread는 스택을 제외한 영역을 메모리 상에서 공유하고, 프로세스는 다른 프로세스와 메모리를 공유하지 않는다. 같은 namespace를 공유하는 프로세스라도 별개의 task이다.

그러나 커널은 프로세스와 스레드를 구분하지 않고, task로 정의한다. 커널 입장에서 스레드는 프로세스에 비해 자원을 더 공유하는 task으로 생각하고, task_struct 구조체를 사용해서 관리한다. 스레드와 프로세스 모두 생성될 때 마다 구조체가 할당된다.

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
        /*
         * For reasons of header soup (see current_thread_info()), this
         * must be the first element of task_struct.
         */
        struct thread_info              thread_info;
#endif
        unsigned int                    __state;
        /* saved state for "spinlock sleepers" */
        unsigned int                    saved_state;
        /*
         * This begins the randomizable portion of task_struct. Only
         * scheduling-critical items should be added above here.
         */
        randomized_struct_fields_start
        void                            *stack;
        refcount_t                      usage;
        /* Per task flags (PF_*), defined further below: */
        unsigned int                    flags;
        unsigned int                    ptrace;
#ifdef CONFIG_SMP
        int                             on_cpu;
        struct __call_single_node       wake_entry;
        unsigned int                    wakee_flips;
        unsigned long                   wakee_flip_decay_ts;
        struct task_struct              *last_wakee;
        ...

프로세스 스케쥴링, 메모리 할당 등 커널의 모든 부분에서 사용한다. 멤버로 가지고 있는 태스크 변수들 중 대표적으로 다음과 같은 것들이 있다.

  • pid : task마다 고유한 값을 가지는 프로세스 ID. 스레드도 하나의 task이기 때문에 고유한 pid(=tid)를 가진다.
  • tgid : thread group ID로, 같은 프로세스에서 생성된 스레드들은 모두 해당 프로세스의 PID 값을 가진다.
  • comm[TASK_COMM_LEN] : task의 실행 파일 명을 저장하는 문자열
  • list_head tasks : task list를 관리하는 헤드로, 순회할 때 시작점
  • mm : 태스크 메모리 관리를 위한 구조체 mm_struct의 포인터
  • files : task가 open()한 파일들을 관리하는 구조체 file_struct의 포인터로, task가 열고 있는 파일의 목록과 각 파일의 상태를 관리한다.
  • signal : 시그널을 처리하기 위한 정보를 포함하는 구조체인 signal_struct의 포인터로, task가 받은 시그널과 시그널 처리 방법을 관리한다.

현재 실행 중인 태스크의 task_struct 포인터를 반환해주는 매크로로 current 매크로가 전역적으로 정의가 되어 있어 다음과 같이 커널 실행 중 namespace 관련 정보를 가져올 수 있다.

static void *net_grab_current_ns(void)
{
    struct net *ns = current->nsproxy->net_ns;    // current를 사용하여 net ns를 가져옴
#ifdef CONFIG_NET_NS
    if (ns)
        refcount_inc(&ns->passive);
#endif
    return ns;
}

Linux Memory Architecture

mm_struct 구조체를 살펴보면 각 프로세스의 대략적인 구조를 알 수 있다.

struct mm_struct {
        struct {
                /*
                 * Fields which are often written to are placed in a separate
                 * cache line.
                 */
                ...
                unsigned long start_code, end_code, start_data, end_data;
                unsigned long start_brk, brk, start_stack;
                unsigned long arg_start, arg_end, env_start, env_end;
                ...
        } __randomize_layout;
        /*
         * The mm_cpumask needs to be at the end of mm_struct, because it
         * is dynamically sized based on nr_cpu_ids.
         */
        unsigned long cpu_bitmap[];
};

code/data/heap(brk) segment의 시작과 끝 주소를 멤버 변수로 가지고 있다.
또한 stack의 시작 주소를 가지고 있어, mm 구조체를 통해서 메모리 구조의 정보를 관리할 수 있다.

이런 유저 영역의 메모리 구조는 다른 프로세스가 같은 가상 주소에 접근하더라도 다른 독립적인 데이터를 읽어오도록 해야 한다. 반먄 커널의 경우 모든 프로세스가 공유하기 때문에 커널 영역의 주소는 서로 다른 프로세스에 접근하면 같은 영역에 접근하게 된다.

가상 주소는 커널이 프로세스에 제공하는 주소 공간으로, 실제 메모리 크기와 독립적으로 존재하며 하나의 프로세스는 자신이 전체 메모리를 독접하는 것 처럼 동작하게 한다. 메모리 관리 장치 (MMU)에 의해 가상 주소는 물리 주소로 변환된다.

  ========================================================================================================================
      Start addr    |   Offset   |     End addr     |  Size   | VM area description
  ========================================================================================================================
                    |            |                  |         |
   0000000000000000 |    0       | 00007fffffffffff |  128 TB | user-space virtual memory, different per mm
  __________________|____________|__________________|_________|___________________________________________________________
                                                              |
                                                              | Kernel-space virtual memory, shared between all processes:
  ____________________________________________________________|___________________________________________________________
                    |            |                  |         |
   ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)
   ffffc90000000000 |  -55    TB | ffffe8ffffffffff |   32 TB | vmalloc/ioremap space (vmalloc_base)
  __________________|____________|__________________|_________|____________________________________________________________
                                                              |
                                                              | Identical layout to the 56-bit one from here on:
  ____________________________________________________________|____________________________________________________________
   ffffffff80000000 |   -2    GB | ffffffff9fffffff |  512 MB | kernel text mapping, mapped to physical address 0
   ffffffffa0000000 |-1536    MB | fffffffffeffffff | 1520 MB | module mapping space
  __________________|____________|__________________|_________|___________________________________________________________

위 메모리 맵은 x86-64 arch의 4 label Page table으로, kernel의 대략적인 메모리 구조를 보여준다. user space 영역이 위치하는 가상 주소를 제외하고, 커널은 다음과 같이 메모리 영역을 나눌 수 있다.

  • 물리적인 RAM을 나타내는 물리 메모리 영역
  • vmalloc 호출 시 할당받는 주소 영역
  • kernel image가 위치라는 kernel의 code 영역
  • device criver 등의 모듈이 매핑되는 kernel module 영역

SLAB allocator

slab 할당자는 커널의 동적 메모리 할당 관리를 위한 시스템으로, 효율적인 메모리 사용과 빠른 할당 해제를 목적으로 한다. 커널 영역에서kmalloc(size, flag)을 호출하여 slab으로부터 동적으로 메모리를 할당받을 수 있고 kfree(ptr)를 통해 해제할 수 있다.

과거에는 slab, slub, slob 할당자로 분류하여 사용하였지만 코드 파편화를 막기 위해 최근은 slub으로 통일하여 다른 할당자는 사용하지 않는 추세이다. slab으로 부르는 것들은 실질적으로 slub인 것이다.

slab allocator의 주요 구성 요소는 다음과 같다.

  • Slab Object : 커널에서 task_structmm_struct 등을 kmalloc() 으로 할당받을 때 반환받는 객체이다. 즉 slab cache에서 할당해 놓은 메모리이다. 효율적으로 할당된 메모리를 관리하기 위해서 할당자는 객체를 조건에 맞게 미리 정의된 크기에 따라 할당해 놓고 사용한다.
  • buddy alloactor : 페이지 프레임 단위 (4KiB)로 메모리를 할당해주는 할당자로, 큰 메모리 블록을 관리하기 위한 할당자이다.
  • Slab Page : buddy allocator로부터 페이지를 할당받아 구성되는 영역으로 슬랩 객체를 관리하는 페이지이다. slab allocator는 페이지를 받아 객체로 분할하여 사용하게 된다.
  • Slab Cache : 특정 크기로 구성된 slab page의 집합이다. kernel에서 자주 사용하는 구조체에 대한 동적 메모리를 미리 확보하고 관리하는 주체이다.
  • Per-CPU, Per-Node : slab cache가 관리하는 슬랩 페이지의 단위로, node별로도 관리하고 CPU별로도 관리하여 빠른 할당을 할 수 있게 한다.
  • freelist : 해제된 slab 객체들 끼리 연결되는 list이다.
  • partial list : 일부 할당된 Object가 남아 있는 Slab Page들을 관리하는 list. 완전 차있거나 완전 비어 있는 페이지 제외된다.
kmalloc(size)
      │
      ▼
   [Slab Cache] ──(object available?)──▶ Object ──▶ caller
        │
        └──(no free object)─────────▶ [Slab Page (via Buddy)] ──▶ Object ──▶ caller

kfree(obj) ──▶ Object ──▶ [Slab Cache]

kmalloc은 요청 크기에 맞는 cache를 선택하여, cache 내부의 slab page에 분할된 object를 선택하여 반환하게 된다. 메모리에서 해제되는 객체는 freelist 또는 per-CPU cache에 저장되어 재사용 때 사용할 수 있게 하며, slab page가 완전히 비게 되는 순간 buddy allocator에게 반환된다.

위 정보들은 /proc/slabinfo에서 모니터링할 수 있으며, 보통은 2의 거듭제곱 단위로 메모리를 할당한다. (성능 최적화를 위함)

추가 정리 : https://velog.io/@marchen/SLAB-Allocator


Linux Kernel Image

부트로더가 메모리에 적재하여 실행하는 커널과 그 주변 구성 요소를 하나의 바이너리 형태로 묶은 파일이다.

zImage, bzImage 형태로 압축된 상태에서, 부팅 시점에 부트 로더에 이미지를 로드하고 setup 과정을 통해 압축을 풀고 메모리에 적재하여 부트 로더가 제어권을 이양함과 동시에 커널을 실행하게 된다.

setup 코드는 부팅 초기화 코드를 포함하는 커널 헤더의 일부분으로, CPU 설정, 메모리 레이아웃 정보, 보호 모드 및 페이징 설정 등 부트로더와 통신하여 커널을 설정한다.

  • vmlinux : 압축되지 않은 리눅스 커널의 실행 파일(ELF). 심볼이 지워지지 않아 디버깅을 수월하게 할 수 있다.
  • bzImage : 큰 커널 이미지를 지원하기 위해 압축 알고리즘으로 압축된 형태의 이미지 파일. 부팅할 때 사용한다.

Kernel Debugging

tools

  • qemu : 아키텍처를 에뮬레이션하는 데 사용되는 가상 머신
명령어설명
qemu-system-x86_64x86-64 아키텍처를 위한 QEMU 가상 머신 실행
-m 4G가상 머신에 4 GB의 메모리를 할당
-smp 4,cores=4,threads=1가상 머신이 4개의 코어를 갖고 각 코어당 스레드가 1개임을 명시
-kernel ./bzImage가상 머신에서 사용할 리눅스 커널 이미지(bzImage)의 경로를 지정
-initrd ./rootfs.cpio초기 RAM 디스크(initrd)로 사용할 파일 시스템 이미지(rootfs.cpio)의 경로를 지정
-append "..."커널 부트 파라미터를 지정
-netdev user,id=t0
-device e1000,netdev=t0,id=nic0
user 네트워크 백엔드를 사용하며, e1000 이더넷 카드를 가상 머신에 연결하는 가상 네트워크 구성
-nographic그래픽 출력 없이 텍스트 기반 콘솔만 사용
-enable-kvm하드웨어 가속을 활성화하여 KVM (Kernel-based Virtual Machine)을 사용
-sGDB를 통해 가상 머신 내부에서 실행 중인 프로세스를 디버깅하기 위해 QEMU를 GDB 서버 모드로 실행, 1234번 포트를 사용
  • busybox : 다양한 표준 UNIX 유틸리티를 하나의 작은 실행 파일로 결합한 프로그램으로, 이를 바탕으로 Root File System을 빋드할 수 있다. 이는 \dev, /usr 등의 디렉토리가 있는 최상위 디렉토리에 마운트되는 파일 시스템으로, 리눅스가 부팅괴디 위한 필수 조건이다.

  • gef : GDB Enhanced Features는 동적 분석 및 exploit 개발 프로세스를 지원하기 위해 python API를 gdb에 추가하여 기능을 제공하는 플러그인이다. pwndbg보다 커널 디버깅에 특화된 플러그인이다.

명령어설명
mount -t proc none /proc .../proc, /sys, /dev 디렉토리를 마운트합니다.
exec 0 ... 2 </dev/console표준 입력, 표준 출력, 표준 에러를 /dev/console로 리디렉션합니다.
echo "7 4 1 7" > /proc/sys/kernel/printk커널 로깅 레벨을 설정합니다. 이로 인해 일반 유저도 커널 메시지를 확인할 수 있어 디버깅이 용이해집니다.
cp /proc/kallsyms /tmp/kallsyms커널 심볼의 주소를 담고 있는 /proc/kallsyms/tmp로 복사합니다. 일반 유저는 해당 파일에서 심볼 주소를 읽을 수 없으나, root 권한으로 복사하면 읽을 수 있게 되어 디버깅에 용이합니다.
setsid cttyhack setuidgid 1000 sh새로운 세션을 시작하고, 사용자 권한(uid 1000)으로 셸을 실행합니다.

빌드하면 rootfs.cpio 이 최종적으로 나오게 된다. 커널 이미지, qemu shell script와 같은 디렉토리에 놓게 되면 커널을 부팅할 수 있는 가상 환경을 구축하게 된다.

Debugging

커널 디버깅을 위한 디버거는 세 가지로 분류된다.

  • kernel debugger : serial(직렬) 포트나 네트워크 등을 통해 커널에 직접 디버깅 명령을 내리는 디버거. kernel과 debugger가 같은 메모리에 위치하게 되어 커널이 멈춰야 디버거가 작동하고, 여러 동작이 제한되는 등 실제 사용이 어렵다.
  • Hardware debugger : 하드웨어 점검에 사용되는 JTAG 등을 통해 회로의 상태를 직접 조회하고 변경하는 디버거. 별도의 디버깅 장비가 필요하다.
  • VM Debugger : 별도의 장비 필요 없이, VM 위에서 커널을 띄우고 host에서 디버깅을 하는 방식이다.

QEMU -s option으로 포트를 열어 gdb remote attach 기능을 사용하여 커널을 디버깅할 수 있다.

gdb 명령어설명
file vmlinuxvmlinux 파일을 디버깅 대상으로 로드 vmlinux는 압축되지 않은 리눅스 커널 이미지로, gdb에 커널 디버그 심볼을 등록할 수 있음
set arch i386:x86-64:intelGDB가 x86-64 아키텍처 (인텔 방식)를 사용하여 vmlinux를 해석
target remote localhost:1234gdb를 원격 시스템에 연결하도록 지시하고 커널이 부팅된 QEMU에 연결됨

위 명령을 활용하면 gdb가 QEMU에 붙어 kernel을 디버깅할 수 있게 된다.

qemu를 키면 root로 접속이 되고 user를 추가할 수 있다.
useraddmuseruseradd -m userpasswd user

syscall도 arch/x86/entry/syscalls/syscall_64.tbl을 수정하는 것으로 추가할 수 있다. 추가하고 빌드하면 해당 syscall을 번호로 호출할 수도 있다.

profile
notion이 나은듯

0개의 댓글