#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.
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
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
Dalam infinite loop ini, program tidak melakukan interaksi apapun dengan kernel.
while True:
pass
Bisa dilihat pada cpu1, utilisasi user space nya 100%
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
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
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
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
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
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
Hmmm
https://0pointer.net/blog/projects/systemd.html
https://0pointer.net/blog/projects/the-biggest-myths.html
X11
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.
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.
Sometimes json output from llm like this : {"key": "value"}``` that langchain JsonOutputParser regex cant…
Version 3 have different interface Example: https://github.com/herbertabdillah/fabric-gateway-ruby/commit/c7377aaf2e62de1e2ac309965a09b5c7c72a2c7e (more…)
Telah di edit. Sumber Asli : https://twitter.com/nateberkopec/status/1250603032523370496/photo/1 Ruby on Rails merupakan framework web MVC menggunakan…
Sebenernya spring boot sudah di embedd applicatoin server tomcat (hanya berisi web container, tidak bisa…