LTS : 장기간의 지원을 제공하는 커널 버전으로, 일정 기간동안 보안 업데이트, 버그 수정 등의 패치가 지속적으로 제공되어 안정적인 환경을 유지해줌.
Stable : 새로운 기능을 빠르게 추가해주는 커널로, 최신 하드웨어 및 네트워크 기능이 필요한 시스템에서 사용한다. 지원 기간은 LTS보단 조금 짧다.
커널 프로젝트는 각 버전별로 stable branch를, 그 중 특정 버전을 LTS로 지정하여 장기 지원을 별행한다.
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;
}

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 영역이 위치하는 가상 주소를 제외하고, 커널은 다음과 같이 메모리 영역을 나눌 수 있다.
slab 할당자는 커널의 동적 메모리 할당 관리를 위한 시스템으로, 효율적인 메모리 사용과 빠른 할당 해제를 목적으로 한다. 커널 영역에서kmalloc(size, flag)을 호출하여 slab으로부터 동적으로 메모리를 할당받을 수 있고 kfree(ptr)를 통해 해제할 수 있다.
과거에는 slab, slub, slob 할당자로 분류하여 사용하였지만 코드 파편화를 막기 위해 최근은 slub으로 통일하여 다른 할당자는 사용하지 않는 추세이다. slab으로 부르는 것들은 실질적으로 slub인 것이다.
slab allocator의 주요 구성 요소는 다음과 같다.
task_struct나 mm_struct 등을 kmalloc() 으로 할당받을 때 반환받는 객체이다. 즉 slab cache에서 할당해 놓은 메모리이다. 효율적으로 할당된 메모리를 관리하기 위해서 할당자는 객체를 조건에 맞게 미리 정의된 크기에 따라 할당해 놓고 사용한다.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
부트로더가 메모리에 적재하여 실행하는 커널과 그 주변 구성 요소를 하나의 바이너리 형태로 묶은 파일이다.
zImage, bzImage 형태로 압축된 상태에서, 부팅 시점에 부트 로더에 이미지를 로드하고 setup 과정을 통해 압축을 풀고 메모리에 적재하여 부트 로더가 제어권을 이양함과 동시에 커널을 실행하게 된다.
setup 코드는 부팅 초기화 코드를 포함하는 커널 헤더의 일부분으로, CPU 설정, 메모리 레이아웃 정보, 보호 모드 및 페이징 설정 등 부트로더와 통신하여 커널을 설정한다.
| 명령어 | 설명 |
|---|---|
qemu-system-x86_64 | x86-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)을 사용 |
-s | GDB를 통해 가상 머신 내부에서 실행 중인 프로세스를 디버깅하기 위해 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와 같은 디렉토리에 놓게 되면 커널을 부팅할 수 있는 가상 환경을 구축하게 된다.
커널 디버깅을 위한 디버거는 세 가지로 분류된다.
QEMU -s option으로 포트를 열어 gdb remote attach 기능을 사용하여 커널을 디버깅할 수 있다.
| gdb 명령어 | 설명 |
|---|---|
file vmlinux | vmlinux 파일을 디버깅 대상으로 로드 vmlinux는 압축되지 않은 리눅스 커널 이미지로, gdb에 커널 디버그 심볼을 등록할 수 있음 |
set arch i386:x86-64:intel | GDB가 x86-64 아키텍처 (인텔 방식)를 사용하여 vmlinux를 해석 |
target remote localhost:1234 | gdb를 원격 시스템에 연결하도록 지시하고 커널이 부팅된 QEMU에 연결됨 |
위 명령을 활용하면 gdb가 QEMU에 붙어 kernel을 디버깅할 수 있게 된다.
qemu를 키면 root로 접속이 되고 user를 추가할 수 있다.
passwd user
syscall도 arch/x86/entry/syscalls/syscall_64.tbl을 수정하는 것으로 추가할 수 있다. 추가하고 빌드하면 해당 syscall을 번호로 호출할 수도 있다.