#include <stdio.h>
int main() {
FILE *f_ptr;
char f_content[256];
char output[512];
f_ptr = fopen("/tmp/1337", "r");
fgets(f_content, 256, f_ptr);
fclose(f_ptr);
sprintf(output, "File content: %s", f_content);
printf("%s", output);
return 0;
}
package main
import (
"fmt"
"io/ioutil"
)
func main() {
b, _ := ioutil.ReadFile("/tmp/1337")
output := "File content: " + string(b)
fmt.Print(output)
}
file = open('/tmp/1337', 'r')
output = "File content: " + file.read()
print(output)
file.close()
Diatas adalah program untuk membaca file dan menampilkannya. Ditulis menggunakan bahasa C, Golang, dan Python. Kode tersebut terlihat simpel. Kita tidak perlu tahu file itu diletakan di disk dan partisi yang mana. Apakah menggunakan filesystem ext4, ntfs, atau fat?. Apakah disknya Hard Drive, SSD, atau memory card?. Karena hal tersebut sudah di abstraksikan oleh Kernel. (Alokasi Memori) Linux bisa dibagi menjadi dua: Userspace, dan Kernel space.
Tugas kernel adalah mendistribusi resource (CPU dan Memory) kepada program dan abstraksi hardware. Mendistribusi resource membuat banyak program bisa berjalan beriiringan (multitasking). Abstraksi hardware, salah satu contohnya seperti yang disebut tadi program tidak perlu tahu detil dari disk, partisi, filesystem untuk mengakses file. Diluar kernel, berarti adalah userspace.
Aplikasi melakukan system call(syscall) untuk berinteraksi dengan kernel. Dalam kasus diatas, dibutuhkan akses ke hardware untuk membaca file dari disk. Serta akses ke terminal untuk menampilkan isi file.
Source code C dan Go di compile menjadi executable. Executable di windows formatnya exe dan di linux ELF (Walaupun tidak ada ekstensi nya). Executable ini yang memanggil system call. Untuk python, source nya di interpretasi oleh Python VM. Lalu Python VM ini yang memanggil system call.
Percobaan 1 : Strace dan Objdump
strace adalah program untuk menampilkan syscall yang dilakukan pada program. Dibawah kita coba melakukan strace pada 3 program tadi (c, go, python)
➜ gcc -o hello-c-syscall hello-syscall.c && strace -o hello-c-strace ./hello-c && cat hello-c-strace | grep -E '^(write|read|close|open|lseek)' | tail
➜ go build -o hello-go hello.go && strace -o hello-c-strace ./hello-go && cat hello-c-strace | grep -E '^(write|read|close|open|lseek)' | tail
➜ strace -o hello-c-strace python3 hello.py && cat hello-c-strace | grep -E '^(write|read|close|open|lseek)' | tail
openat(AT_FDCWD, "/tmp/1337", O_RDONLY) = 3
read(3, "hello world", 4096) = 11
read(3, "", 4096) = 0
close(3) = 0
write(1, "File content: hello world\377N", 27) = 27
openat(AT_FDCWD, "/tmp/1337", O_RDONLY|O_CLOEXEC) = 3
read(3, "hello world", 512) = 11
read(3, "", 501) = 0
close(3) = 0
write(1, "File content: hello world", 25) = 25
openat(AT_FDCWD, "/tmp/1337", O_RDONLY|O_CLOEXEC) = 3
lseek(3, 0, SEEK_CUR) = 0
lseek(3, 0, SEEK_CUR) = 0
read(3, "hello world", 12) = 11
read(3, "", 1) = 0
write(1, "File content: hello world\n", 26) = 26
close(3) = 0
Walaupun ketiga bahasa tersebut bebeda, mereka memanggil system call yang sama contohnya openat. Untuk membuka file pada path /tmp/1337. openat mereturn alamat (File Descriptor) dari file tersebut, yaitu 3. Kemudian read dipanggil dengan paramter file descriptor tadi (3) untuk membaca file tersebut.
Umumnya kita tidak memanggil syscall secara langsung. Melainkan menggunakan library yang lebih mudah. Biar library itu yang memanggil system call nya. Contoh kode di kanan ini adalah kode C yang langsung menggunakan syscall. Terlihat sangat mirip dengan hasil strace tadi.
#include <stdio.h>
int main() {
FILE *f_ptr;
char f_content[256];
char output[512];
f_ptr = fopen("/tmp/1337", "r");
fgets(f_content, 256, f_ptr);
fclose(f_ptr);
sprintf(output, "File content: %s", f_content);
printf("%s", output);
return 0;
}
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
char f_content[256], output[512];
int fd, size;
fd = openat(AT_FDCWD, "/tmp/1337", O_RDONLY);
size = read(fd, f_content, 256);
close(fd);
f_content[size] = '\0';
sprintf(output, "File content: %s", f_content);
write(1, output, strlen(output));
return 0;
}
Dalam linux ada dua cara memanggil syscall. Pertama dengan instruksi khusus pada assembly/binary. Kedua dengan library pada bahasa C. Karena python dibuat dari bahasa C, python menggunakan library syscall C juga. Sedangkan golang, memakai syscall menggunakan assembly/binary. Akan di jelaskan detailnya di bawah.
Dokumentasi detail dari system call bisa dilihat disini: https://man7.org/linux/man-pages/dir_section_2.html . Atau menggunakan command man 2 $nama_syscall di linux
Percobaan 2: top: User time, kernel time, io wait time
Kita akan melakukan eksperiman membuat empatprogram Python: Infinite loop, infinite loop dengan banyak thread, membaca file dengan simulasi disk I/O yang pelan, dan membaca file besar 4gb dengan ram 2gb. Ketika program berjalan, kita monitor menggunakan tools top (task managernya linux). Metric yang diperhatikan adalah Utilisasi % CPU. Dalam linux utilisasi cpu dibagi ke beberapa bagian. Hal yang kita perhatikan adalah
- % us (User Space): % waktu cpu yang dihabiskan di user space (aplikasi kita)
- % sy (kernel Space) % waktu cpu yang dihabiskan di kernel space oleh kernel
- %wa (wait I/O disk time): % waktu cpu yang idle. namun ketika idle kernel masih mengunggu I/O dari disk.
- Sebenernya masih ada utiliasi lain seperti ni, hi, si, st
1. Infinite Loop
Dalam infinite loop ini, program tidak melakukan interaksi apapun dengan kernel.
while True:
pass
Bisa dilihat pada cpu1, utilisasi user space nya 100%
2. Infinite Loop Dengan Banyak Thread
Sama seperti tadi, namun kita membuat banyak (100 ribu) OS thread untuk menjalankan infinite loop tersebut. Sehingga infinite loop ini bisa berjalan parallel.
from threading import Thread
import time
def f():
time.sleep(5)
while True:
pass
threads=[Thread(target=f) for i in range(1, 100000)]
for t in threads:
t.start()
for t in threads:
t.join()
Bisa kita lihat, utiliasi user space cukup tinggi 13 dan 18 %. Namun yang menarik adalah utilisasi kernel space tinggi mencapai 80%. Bedanya dengan yang pertama kita menspawn banyak thread. Tugas kernel adalah mendistribusi resource (cpu/ram) kepada program program. Dalam hal ini adalah 100,000 thread yang kita bikin tadi diatur oleh kernel suapya bisa berjalan beriiringan (multitasking). MIsal dalam jangka waktu 10 ms thread tersebut belum selesai (karena infinite loop, tidak pernah selesai), kernel men “sleep” kan thread tersebut. Dan kernel memberi giliran kepada thread yang lain untuk jalan.
Ada 100,000 thread dan semuanya tidak selesai selesai. Hal ini yang membuat kernel sibuk kewalahan mengatur giliran jalan 100,000 thread tersebut. Sehingga utilisasi cpu di user space jadi tinggi
3. Simulasi Membaca I/O dari Harddisk pelan
Di linux kita bisa melakukan simulasi membuat disk yang I/O nya sangat pelan. Untuk detil caranya bisa lihat disini:
Kemudian file tersebut kia baca di python
import random
f = open("/dev/mapper/dm-slow", "r")
byte_file_size = 100 * 1024 * 1024
while True:
f.read(random.randint(1, byte_file_size))
Bisa kita lihat, semua nya nganggur kecuali utilisasi cpu yang idle namun masih ada antrian I/O (wa). 100% wa bukan berarti cpu sibuk. Namun sama seperti idle. Jadi CPU Idle 100%. Hanya saja beda nya dengan idle, disini masih ada antrian I/O dari disk yang menumpuk
4. Membaca File Besar 4 gb (Tanpa simulasi pelan)
Masih sama seperti tadi. Hanya saja tidak dibikin simulasi pelan. Kita membaca file 4 gb. Sedangkan ram di sistem hanya 2 gb. Bisa kita lihat utiliasi cpu di kernel space (sy) dan wait I/O disk time (wa) cukup tinggi.
Hal ini dikarenakan jika memory habis, kernel akan menggunakan swap. Yaitu memory yang diletakan di disk. Kecapatan disk sangat jauh pelan dibandingkan RAM. Sehingga kernel sibuk memindahkan antar ram dan swap. Dan I/O disknya menjadi berat juga
Sebenernya ada cara juga untuk membaca file besar tanpa membutuhkan memory yang sama dengan besar file nya. Menggunakan mmap
Kesimpulan
- Sistem operasi membagi area (virtual) memori menjadi dua: User space dan kernel space
- Tugas kernel mendistribusi resource (CPU/RAM) dan abstraksi hardware
- Userspace tempat aplikasi kita berjalan
- Userspace memanggil kernel menggunakan system call
- Masih Penasaran? Coba lanjut baca dibawah ini
Kosa Kata Baru
- strace: command linux untuk mengetahui system call yang dilakukan pada suatu program
- file descriptor: semua I/O di linux baik itu file atau pun network di dalam satu proses di identifikasikan dengan angka
- man: command linux untuk melihtat dokumentasi dari command dan library C header
- top: command linux untuk melihat utlisasi cpu, ram ,io dan proses
- us
- sy
- wa
- mmap
Encore 1 – User Space Yang Penting
Berikut statistik sistem Ubuntu linux di breakdown jumlah baris kode sumber nya (source line of code).
Terlihat kernel sangat kecil sekali proporsinya, karna tugas nya yang sebenernya simpel. Dengan hanya kernel, tidak ada yang bisa digunakan. Begitu juga sebalik nya. Bahkan kalau di breakdown lagi di dalam kernel. Hanya 35% kode yang berkaitan tentang kernel. 50% berisi driver, dan 15% berisi kode spesifik assembly kernel untuk tiap arsitektur (x86, arm, powerpc, dll). Sumber
Userspace yang akan dibahas adalah Libc dan Init system. Userspace berikut membedakan secara fundametal tiap distribusi linux
Libc, Static Linked, Dynamic Linked
FILE *f_ptr;
char f_content[256], output[512];
f_ptr = fopen("/tmp/1337", "r");
fgets(f_content, 256, f_ptr);
fclose(f_ptr);
char f_content[256], output[512];
int fd, size;
fd = openat(AT_FDCWD, "/tmp/1337", O_RDONLY);
size = read(fd, f_content, 256);
close(fd);
f_content[size] = '\0';
Pada contoh sebelumnya kita membaca file menggunakan bahasa C melalui 2 cara: mengguankan library fopen, fgets, fclose. Cara kedua langsung menggunakan syscall openat, read, close. Sebetulnya library fopen yang kita pakai tadi di belakang layar juga memanggil system call openat.
Umumnya program depend ke library utama bahasa C. Bahkan bahasa lain pun seperti python juga depend ke C. Karena Python VM dibuat dengan bahasa C. Library utama ini disebut LIBC atau C Standard Library. Contoh isi dari libc adalah
- System Call wrapper. Jadi tidak perlu menggunakan assembly
- math.h untuk operasi matematika seperti absolut, sin, cos, tan.
- string.h untuk manipualsi string. strcat untuk menggabungkan string, strcmp untuk membandingkan string
C dan Python (VM Python3) menggunakan glibc dengan cara dynamic linking. Yaitu binary hasil compile nya tidak berisi glibc. Melainkan berisi nama library yang dibutuhkan saja, yaitu glibc. Ketika binary ini dijalankan, sistem akan mencari library yang dibutuhkan, yaitu libc dalam sistem. Dan program ini berjalan.
ldd digunakan untuk mengetahui apakah binary nya butuh dependensi library (dynamic linked). Dibawah ini binary dari hello c dan python3 VM bergantung ke libc.so.6, yaitu GLIBC. Sedangkan go tidak (not a dynamic executable, berarti Static LInked). Karena go memanggil syscall langsung menggunakan assembly.
objdump digunakan untuk melihat assembly code dari binary. Bisa dilihat untuk hello-c tidak ada instruksi syscall. Karena hello-c menggunakan library glibc. Sehingga jika kita lihat objdump dari glibc, ada instruksi syscall. Dan jika kita liaht objdump dari hello-go, terdapat instruksi syscall.
➜ ldd ./hello-c
...
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
...
➜ objdump -d ./hello-c | grep -E "syscall " -B 5| head
➜ objdump -d /lib/x86_64-linux-gnu/libc.so.6 | grep -E "syscall " -B 5| head
...
29db2: 89 d0 mov %edx,%eax
29db4: 0f 05 syscall
➜ ldd $(which python3)
...
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
...
➜ ldd ./hello-go
not a dynamic executable
➜ objdump -d ./hello-go | grep -E "syscall " -B 5| head
...
402789: 48 89 df mov %rbx,%rdi
40278c: 0f 05 syscall
libc yang umum digunakan sampai saat ini adalah GLIBC (GNU Lib C). Kedua adalah musl yang dipakai di Alpine Linux (sering digunakan di container). musl dibuat karena GLIBC dirasa terlalu kompleks kode nya. Misal optimasi yang berlebihan di fungsi strcmp yang berfungsi untuk membadingkan dua string. Di glibc, terdapat banyak versi strcmp sesuai arsitektur nya menggunakan bahasa assembly. Sedangkan di musl, hanya satu menggunakan C. Biar compiler yang menterjemahkan ke arsitektur.
Optimasi pada glibc membuat glibc lebih cepat dibandingkan musl. Namun, Kompleksitas ini menyebabkan dua masalah: ukuran dan keamanan. Karena ukuran nya kecil, musl cocok untuk digunakan di embeded device. Semakin kompleks kode, pasti akan membuka celah keamanan yang lebih luas. Contoh GHOST vuln glibc
Init System
Hmmm
https://0pointer.net/blog/projects/systemd.html
https://0pointer.net/blog/projects/the-biggest-myths.html
Display System
X11
Kesimpulan
- zzz
Kosa Kata Baru
- ldd: command linux untuk mengetahui library apa yang di link secara dinamis pada executable binary tersebut
- libc
- static linked, dynamic linked
Encore 2 – Hybrid, Monolithic, Microkernel
Desain dari kernel linux adalah monolithic. Berarti semua komponen kernel jalan dalam satu binary file dan menggunakan alamat memori yang sama berbarengan. Sedangkan Microkernel, tiap component kernel adlah binary yang berbeda serta memori yang beda pula. Komunikasi antar komponennya menggunakan IPC (inter processs communication).
Monolithic pasti secara performa lebih cepat. Namun kesalahan pada satu modul dapat menggagu menganggu kernel.
Kernel Module
Bisa menambahkan modul kernel secara realtime. Tidak perlu compile kernel dan restart. Walaupun berbeda file dengan kernel. Kernel module tetap jalan pada memori yang sama dengan kernel.
Contohnya adalah driver. Umumnya driver diletakan di kode utama kernel linux. Dan di kompilasi bersama kernel juga menajdi satu binary kernel. Sehingga kode driver jadi open source. Manufacturer seperti NVIDIA enggan mengopensource kan driver mereka dan meletakan nya bersamaan kode kernel. Sehingga driver NVIDIA menggunakan kernel module. Dan tiap kali ada update pada versi kernel, driver ini harus di “compile” ulang juga. Tidak seperti di windows. Untungnya sudah ada mekanisme compile otomatis setiap kernel update bernama DKMS
https://en.wikipedia.org/wiki/Dynamic_Kernel_Module_Support
Interface kernel linux sering berubah tidak seperti windows. Hal ini yang menyebabkan kernel module harus di compile ulang setiap kali kernel update. Mengapa sering berubah?
https://www.kernel.org/doc/Documentation/process/stable-api-nonsense.rst
Contoh dibawah ini adalah kode C yang membuat array. Lalu kita akses array tersebut melebihi kapasitas nya. Misal ada 5 element, kita akses elemen 10. Di bahasa lain, akan terjadi error karena bahasa yang type safe. Terdapat pengecekan (bound check).
Kode dikiri adalah aplikasi biasa. Sedangkan di kanan adalah kernel module.
#include<stdio.h>
#include<stdlib.h>
void run(void) {
int i;
int *arr = (int*) malloc(5 * sizeof(int));
for(i = 0; i < 5; i++) {
//arr[i] = 0;
*(arr + i) = i;
}
printf("5 element done\n");
for(i = 0; i < 100000000; i++) {
//arr[i] = i;
*(arr + i) = 1;
}
printf("outside bound done\n");
}
int main() {
run();
return 0;
}
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/mm.h>
void run(void) {
int i;
int *arr = (int*) kvmalloc(5 * sizeof(int), GFP_KERNEL);
for(i = 0; i < 5; i++) {
//arr[i] = 0;
*(arr + i) = 0;
}
printk(KERN_INFO "5 element done");
for(i = 0; i < 100000; i++) {
//arr[i] = 1;
*(arr + i) = 1;
}
printk(KERN_INFO " outside bound done\n");
}
static int __init custom_init(void) {
printk(KERN_INFO "init");
run();
return 0;
}
static void __exit custom_exit(void) {
printk(KERN_INFO "exit");
}
module_init(custom_init);
module_exit(custom_exit);
Ketika akses memory yang diakses tidak jauh dengan aslinya, tidak ada error. Namun program corrupt karena …. Bisa terjadi error di waktu kemudian
Ketika akses memory yang sangat jauh, aplikasi kita di keluarkan oleh OS (Linux) dengan error segmentation fault. Hal ini adalah fitur virtual memory yang di kelola oleh kernel linux.
Sedangkan di kernel module, tidak ada segmentation fault. Meskipun bisa di load secara dinamis tanpa perlu compile ulang kernel dan restart. Hal ini di karenakan kernel dan kernel module berjalan di dalam satu aplikasi dan memori yang sama dengan kernel. Ini yang disebut monolithic kernel. Jadi hal yang mungkin terjadi adalah sistem tidak stabil karena komponoen kernel lain corrupt memory nya. Dan kernel panic.