Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ৯/১৫অ্যাডভান্সড৫৫ মিনিট

Runtime System — main() এর আগে আর পরে কী চলে

Runtime Systems

main() লেখেন আপনি, কিন্তু main-এর আগে execve থেকে _start হয়ে __libc_start_main পর্যন্ত একটা পুরো runtime দৌড়ে মঞ্চ সাজায় — সেই অদৃশ্য স্তরটাই আজকের বিষয়।

এই লেসন শেষে আপনি পারবেন

  • execve → _start → crt0 → __libc_start_main → main → exit চেইনটা ধাপে ধাপে ব্যাখ্যা করতে পারবেন — argc/argv/environ কীভাবে হস্তান্তর হয়, .init_array/.fini_array কখন চলে
  • strace output পড়ে C, Go, Java, Python hello-world-এর main-এর আগে কোন type-এর কয়টা syscall ঘটে তা গুনে তুলনা করতে পারবেন
  • চারটা runtime-এর inventory তুলনা করতে পারবেন — libc (প্রায় শূন্য), Go (G-M-P scheduler + growable stack + integrated GC), JVM (class loader + tiered JIT + heap), CPython (bytecode VM + refcount/cycle GC + GIL) — প্রতিটা OS-এর কোন কাজটা নিজের ভেতরে নিয়েছে
  • -nostdlib দিয়ে compile হওয়া প্রোগ্রামে নিজের _start লিখে inline asm দিয়ে write/exit syscall ডাকতে পারবেন আর exit code echo $? দিয়ে যাচাই করতে পারবেন
  • FFI boundary আর panic/unwind machinery-কে runtime-এর অংশ হিসেবে ব্যাখ্যা করে বলতে পারবেন কেন cross-runtime call (যেমন Go callback-এ C) বিপজ্জনক

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আপনার প্রথম C প্রোগ্রামটা ছিল সম্ভবত:

#include <stdio.h>
int main(void) {
    printf("hello\n");
    return 0;
}

Textbook বলে “program starts at main”। আজ আমরা সেই বাক্যটা ভাঙব। এই প্রোগ্রাম চলতে গেলে kernel execve করে ELF image load করে (assembly module-এর elf-format লেসন), entry point-এ jump করে — আর সেই entry point main না, _start। _start থেকে আরেকটা চেইন দৌড়ায় — argument গোছানো, standard streams খোলা হয়েছে কি না, TLS setup, constructor-দের invocation — তারপর কেউ আপনার main-কে ডাকে। আর return 0-এর পরেও আরেকবার runtime জেগে ওঠে: destructors, atexit handlers, exit_group syscall।

এই পুরো অদৃশ্য স্তরটার নাম runtime system — আপনার ভাষার stdlib/library/runtime যা কিছু, যার কাজ হলো OS-এর সাথে দরকারি সব দরবার সেরে আপনার কোডকে একটা comfortable illusion দেওয়া: “তুমি শুধু logic লেখো, memory/scheduler/startup/shutdown আমি দেখছি।”

মজার ব্যাপার: ভাষাভেদে এই স্তরের size আকাশ-পাতাল। libc-র runtime প্রায় শূন্য — কয়েকশ instruction। Go-র runtime একটা সম্পূর্ণ user-space operating system: নিজের scheduler, নিজের growable stack, integrated GC। JVM তার উপর class loader + JIT compiler। CPython একটা bytecode interpreter যার প্রতিটা opcode execution-ই runtime-এর হাত ধরে চলে।

আজকের experiment-এ strace দিয়ে চার ভাষার startup-syscall গুনবেন, gdb-তে _start-এ break করবেন, আর build section-এ libc ছাড়া binary বানিয়ে দেখবেন runtime-টা সরালে ঠিক কী ফেরত লিখতে হয়।

মূল ধারণা

Startup chain — execve থেকে main পর্যন্ত

$ ./hello
   │
   ▼ shell: fork + execve("./hello", argv, envp)
┌─────────────────────────────────────────────────────────┐
│ KERNEL                                                  │
│  1. ELF header parse → valid? interpreter field (.interp)?│
│  2. ld.so load (dynamic linking হলে) → dependencies map  │
│  3. new address space, segments map (r-x, r--, rw-)      │
│  4. stack setup: argc, argv[], envp[], auxv[]            │
│     └ auxv = AT_PHDR, AT_ENTRY, AT_RANDOM...             │
│  5. RIP ← e_entry (= _start)                             │
└─────────────────────────────────────────────────────────┘
   │
   ▼ USER SPACE
_start            (crt1.o — compiler স্বয়ংক্রিয়ভাবে জুড়ে দেয়)
   │  %rsp থেকে argc pop, argv/envp খুঁজে বের করা
   │  __libc_early_init, TLS pointer setup
   ▼
__libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end)
   │  1. thread/TLS infrastructure চূড়ান্ত করা
   │  2. atexit(rtld_fini) — loader-এর cleanup register
   │  3. .preinit_array → .init_array চালানো   ← আপনার constructors!
   │  4. result = main(argc, argv, envp);      ← আপনার কোড, FINALLY
   │  5. exit(result)
   ▼
exit(result)
   │  .fini_array / atexit handlers (LIFO)      ← আপনার destructors
   │  stdio flush, _IO_cleanup
   ▼
exit_group(result)  → kernel: সব thread kill, memory unmap, fd বন্ধ

তিনটা observation:

  1. main একটা convention, entry point না। <Term id="elf">ELF</Term>-এর e_entry যাকে point করে সে-ই প্রথম চলে; linker default-এ crt1.o-র _start বসায়, যেটা <Term id="calling-convention">Calling Convention</Term> মেনে main-কে invoke করে। -e myentry দিলে আপনার নিয়ম।
  2. Constructors/destructors আপনার জানামতেও চলে। __attribute__((constructor)) C-তেও কাজ করে — .init_array-তে function pointer বসায়, __libc_start_main সেগুলো main-এর আগে call করে। Static initializer ভরা C++ program-এর startup ধীর লাগার আসল কারণ এই array-ই।
  3. exit ≠ return। main থেকে return মানে __libc_start_main-কে value ফেরত — সে exit() ডাকে। কিন্তু exit() সরাসরি ডাকলে main-এর বাকি code আর চলবে না, শুধু atexit-chain চলবে। আর কোনো thread থাকলে exit() সব thread মেরে দেয় (exit_group) — যেখানে single-thread pthread_exit শুধু নিজে।

Runtime-এর চুক্তিপত্র — সে ঠিক কী দেয়

প্রতিটা runtime ভিন্ন পরিমাণ সেবা দেয়, কিন্তু menu-টা এই ছয় ক্যাটাগরিতে ফেলা যায়:

সেবাlibc (C)CPythonJVMGo
Startup/shutdowncrt0 + init arrays+ interpreter boot, site import+ class loader hierarchy+ runtime init, P তৈরি
Memorymalloc/free (thin)pymalloc arena + refcount/cycle GCmanaged heap + generational GCspan allocator + concurrent GC
Schedulingনেই (kernel threads)OS threads + GILOS threads (Java 21+ virtual)user-space G-M-P scheduler
Execution modelnative machine codebytecode VM loopJIT tiers (C1/C2)native compile ahead-of-time
Panic/error machineryনেই (errno, signal)exception objects + tracebackexceptions + unwind tablespanic/recover + goroutine dump
FFI boundarydlopen/dlsymctypes/cffi → C ABIJNIcgo (দামি bridge!)

এখানেই আজকের লেসনের কেন্দ্রীয় claim: “C has no runtime” মানে শুধু “C-এর runtime সবচেয়ে ছোট।” শূন্য না — crt0, malloc, stdio buffering, TLS, unwind (C++-এ) সবই runtime। Go/JVM/CPython ঐ একই ছকের ঘরগুলো বেশি ভরে ভরে লিখেছে।

ভেতরে কী ঘটছে

চারটা runtime-এর ভেতরটা

Go — user-space operating system

Go binary-তে go func(){} লিখলে আপনি kernel thread spawn করছেন না। Runtime তিন abstraction রাখে:

G (goroutine)  — একটা কাজ: নিজের stack, PC, state
M (machine)    — একটা OS thread (kernel-এর জিনিস)
P (processor)  — scheduling context: সর্বদা GOMAXPROCS সংখ্যক,
                 M-কে G চালানোর "license" দেয়
Global run queue ──► P(local runq)──attach──► M ──runs──► G
                          ▲                    │ steal
        work-stealing ◄───┴──── অলস M অন্য P-র queue থেকে G চুরি করে
  • Growable stack: প্রতিটা G ২ KB দিয়ে শুরু; function prologue-এ guard check — stack ছোঁয়ার পথে থাকলে runtime-এর morestack বড় stack allocate করে copy করে (pointer-গুলো adjust করে — সম্ভব কারণ GC-র precise stack maps আগে থেকেই জানে কোথায় pointer আছে!)। তুলনা: OS thread-এর fixed ৮ MB virtual reservation — লক্ষ লক্ষ goroutine সেখানে অসম্ভব।
  • Integrated GC: collector আর scheduler একই runtime-এ — GC-র safepoint মানে goroutine-দের cooperative yield; STW মানে সব P থামানো। লেসন ৭-এর sub-ms pause-এর পেছনে এই tight coupling।
  • netpoller: blocking socket read লিখলেও runtime epoll/kqueue-তে register করে G-কে park করে, M-কে অন্য G চালাতে দেয় — synchronous-লাগছে কোড, asynchronous বাস্তবতা।

JVM — class loader, JIT tiers, managed heap

  • Class loader hierarchy: bootstrap → platform → application; lazy loading (first-use), verification (bytecode safety checks!), linking+initialization। ClassCastException আর “loader identity” জটিলতা এখান থেকেই।
  • Tiered JIT: interpreted (~level 0) → C1 profiling client-compiler (level 2-3) → C2 optimizing (level 4)। Hot path profile-guided optimize করে — speculative devirtualization পর্যন্ত; assumption ভাঙলে deopt। আপনার “same Java code” প্রথম run-এ আর হাজারতম iteration-এ ভিন্ন machine code — এই স্তরের কারসাজি।
  • Heap + threads: generational GC (লেসন ৭), আর traditional threads = OS threads (১ MB stack করে); Project Loom-এর virtual threads এখন Go-র G-এর মতো user-space scheduling এনেছে।

CPython — একটা লুপ যা সবকিছু

python3 script.py:
  1. compile → bytecode (.pyc cache)
  2. ceval loop: switch(opcode) — LOAD_FAST, BINARY_OP...
     প্রতিটা instruction-এর আগে-পরে: eval breaker check
     (signal! GIL drop interval! async hook!)
  3. object model = PyObject* — refcount লেসন ৮-এ দেখা
  • GIL: এক mutex, একসাথে এক thread-ই bytecode execute করতে পারে। কারণ historical: refcount-এর atomicity সস্তায় পাওয়া, C extension ecosystem-এর compatibility। ফল: CPU-bound threading parallel না (IO-bound ঠিকই লাভ করে — IO-র সময় GIL ছাড়ে)। sys.setswitchinterval(0.005) — default ৫ ms-এ জোর করে handover।
  • Frame object: প্রতিটা Python call = একটা heap-allocated frame object — traceback inspect করা, generator suspend/resume — সব সম্ভব কারণ activation record ভাষার data হিসেবে বেঁচে আছে।

libc — ন্যূনতম চুক্তি

libc-র runtime-অংশ আসলে কয়েকশ instruction: _start-এ argument unpacking, TLS setup, init-array walk। malloc নিজে library (allocator-টা OS লেসনে পড়েছেন — userspace, kernel জড়িত না যতক্ষণ না brk/mmap লাগে), stdio buffer-ও library। Scheduler নেই, GC নেই, VM নেই। এই খর্বতাই C-কে embedded/kernel-এর ভাষা করেছে — এবং UAF-এর দায় programmer-এর মাথায় ফেলে দিয়েছে (লেসন ৬)।

printf hello — একটা সোজা call-এর স্তরগুলো
  1. Your codeprintf("hello\n") — format parse, stdout buffer-এ copy (buffered! এখনো কোনো syscall নেই)
  2. stdioBuffer full বা newline-এ flush policy → write() wrapper ডাকে
  3. syscall stubwrite-এর syscall number (%rdi=fd, %rsi=buf, %rdx=len) register-এ বসিয়ে syscall instruction
  4. Kernelfd table → file/socket/tty driver → bytes out; ফেরতে ret, errno সেট

চার স্তর একটা print statement-এ। Runtime-এর পুরো দর্শন: প্রতিটা স্তর আগেরটার চেয়ে সামান্য বেশি comfortable illusion দেয় — আর প্রতিটা comfort-এর একটা cost (buffering delay, indirection, ABI lock-in)।

Panic/unwind আর FFI — দুইটা সীমান্ত

Unwinding: Java exception throw করলে runtime stack নামতে নামতে frame-by-frame handler খোঁজে — কীভাবে জানে কোন frame-এ কোন catch? Answer: .eh_frame unwind tables (linker/loader লেসনে দেখা segment) — compiler প্রতিটা nonleaf function-এ metadata রাখে: callee-saved registers কোথায়, return address কোথায়। C++ exception, Rust panic, Java exception, Go panic — সবাই এই একই DWARF-based machinery-র উপর (Go আংশিক)। Zero-cost exceptions নামটা এখান থেকে: happy path-এ শূন্য খরচ, throw-এ টেবিল-walk-এর ব্যয়।

FFI boundary: দুই runtime-এর মধ্যে ডাকাডাকি মানে দুই চুক্তির সংঘর্ষ — calling convention mismatch, object model mismatch (Java object-কে C pointer বলা যায় না), GC mismatch (C-কে ধরে রাখা reference-টা moving GC নাড়িয়ে দিলে?)। JNI-র প্রতিটা GetByteArrayElements, cgo-র প্রতিটা crossing ~50–200 ns + scheduler constraint — এই boundary-ই cross-language system-এর hidden tax line।

উদাহরণ

একই hello world, চারটা runtime — strace-এর চোখে

সবচেয়ে ছোট প্রোগ্রাম চার ভাষায়, আর তাদের main চলার আগের syscall footprint:

# C (static):           strace -c ./hello_c    → ~6 syscalls
# Go:                   strace -c ./hello_go   → ~40+ syscalls
# Java:                 strace -c java Hello   → ~600+ syscalls
# Python:               strace -c python3 h.py → ~500+ syscalls

Typical breakdown (Linux, সংখ্যা distro/version-ভেদে বদলাবে — pattern বদলাবে না):

C static:      execve, mmap(×2 stack/heap), arch_prctl(TLS),
               write, exit_group                        ≈ 6 calls

Go:            C-এর সবটা + runtime setup:
               sched_getaffinity, mmap(×N — spans, gc bitmap),
               sysctl-ish reads, madvise, sigaltstack,
               newosproc (clone ×GOMAXPROCS), write...   ≈ 40–80

Python:        dynamic linking-এর হাজার openat/newfstatat/
               mmap/read-close (libpython + deps),
               site-packages scan, pyc stat, write       ≈ 400–800

Java:          Python-এর সব + libjvm load, huge reserve mmaps
               (heap + metaspace + codecache), thread clone,
               perf counters probe                       ≈ 500–1500

আর gdb দিয়ে _start-এ নেমে দেখুন প্রথম instruction-টাই কী:

gcc -static -o hello_c hello.c
gdb -batch -ex 'break _start' -ex run -ex 'x/8i $pc' -ex 'info registers rsp' ./hello_c
=> 0x401ae0 <_start>:  xor %ebp,%ebp        ← frame pointer base শূন্য
   0x401ae2 <_start+2>: mov %rdx,%r9        ← rtld_fini save
   0x401ae5 <_start+5>: pop %rsi            ← argc!
   0x401ae6 <_start+6>: mov %rsp,%rdx       ← argv & envp head
   ...                                       (__libc_start_main-এ push)
rsp = 0x7fffffffe300   ← শীর্ষে বসে আছে argc, তারপর argv[0..], NULL,
                         envp..., NULL, auxv...

Stack-এর ঠিক ওই layout-টাই System V ABI-র process-initialization contract — এবং পরের BuildIt-এ আমরা এটাই raw পড়ব।

ছবিতে

Runtime-এর জন্ম-মৃত্যু timeline — আর Go-র G-M-P মানচিত্র

Process lifecycle-এ runtime-এর দখলদারি:

execve ──┬── kernel: load, map, stack ──────────────────┐
         │                                              │ RUNTIME
         ├── _start: unpack args, early init            │ এখানেই
         ├── __libc_start_main / runtime.init           │ আপনার
         ├── .init_array / package-level init           │ কোডের
         │                                              │ বাইরে
         ├──────── main()/func main() ──────────────────┤
         │                                              │ RUNTIME
         ├── return path: exit(ret)                     │ আবার
         ├── .fini_array / defer/atexit (LIFO)          │ এখানে
         ├── stdio flush, GC final pass, thread teardown│
         └── exit_group(ret) → kernel reaps ────────────┘

Go-র scheduler — একটা snapshot:

        global runq (overflow)
              │
   ┌──────────▼──────────┐
   │ P1 localrunq [G5,G9]│◄──┐ steal
   │  M1 (OS thread)     │   │
   │   └ running G3 ─────┼─┐ │      ┌──────────────┐
   └─────────────────────┘ │ └──────► P2 [G7] M2 idle│
                           │        └──────────────┘
                      syscall/blocking?
                           │ yes: M1 কে kernel-এ ধরে রাখো,
                           ▼ P1+queue অন্য M-কে দাও (hand-off!)
        netpoller: ready I/O-র G-রা runq-তে ফেরত
আপনার mental model:        বাস্তবতা (Go):

main ──► work ──► done     main(goroutine #1)
                              │ go f()
                              ▼
                           P-queue ──► M(OS thread) ──► CPU
                                          │ morestack? GC assist?
                                          ▼
                                      runtime internals
                                          │
                                          ▼
                                     kernel (syscalls only)
দুইটা ছবি একসাথে পড়ুন — বাইরের ছবিটা 'কখন' runtime চলে, ভেতরেরটা 'কীভাবে' সে কাজ ভাগ করে। Go-র মতো ভাষায় আপনার প্রতিটা function call মানেই এই মানচিত্রের কোনো না কোনো ঘরে পা।

নিজে চালিয়ে দেখুন

EXPERIMENT

strace forensics — চার ভাষার startup autopsy

Linux (strace + gcc + go + java + python3; যা আছে তা দিয়েই চলবে)· ২০ মিনিট

ধাপ ১ — চারটা hello world:

/* hello.c */
#include <stdio.h>
int main(void) { puts("hello"); return 0; }
// main.go
package main
import "fmt"
func main() { fmt.Println("hello") }
public class Hello {
    public static void main(String[] a) { System.out.println("hello"); }
}
gcc -O2 -static -o hello_c hello.c          # static: dynamic-link noise বাদ
gcc -O2 -o hello_cdyn hello.c               # তুলনার জন্য dynamic version
go build -o hello_go main.go
javac Hello.java

strace -c -o s_c.txt    ./hello_c
strace -c -o s_g.txt    ./hello_go
strace -c -o s_j.txt    java Hello
strace -c -o s_p.txt    python3 -c 'print("hello")'

for f in s_c.txt s_g.txt s_j.txt s_p.txt; do echo "== $f"; grep total $f; done

Typical totals:

== s_c.txt     6 calls total
== s_g.txt     45 calls total
== s_j.txt     700+ calls total
== s_p.txt     550+ calls total

ধাপ ২ — type-wise breakdown (main-এর আগে কী ঘটছে বুঝতে):

grep -E '^ *[0-9]+ +(openat|mmap)' s_p.txt | head    # Python-এর linking storm
grep -E 'clone|mprotect|madvise' s_g.txt | head      # Go-র scheduler/GC setup
head -25 s_g.txt                                      # প্রথম syscalls-ই runtime init

Go-র trace-এ খুঁজুন: sched_getaffinity (GOMAXPROCS নির্ধারণ!), sigaltstack (signal handling for stack-growth faults!), clone (Ms spawn)। Python-এর trace-এ: শত শত openat — shared libraries আর modules খোঁজা। এই দুই signature-ই ওই ভাষার runtime-এর আঙুলের ছাপ।

ধাপ ৩ — gdb-তে _start (macOS-এ lldb দিয়ে একই ভাবনা):

gdb -batch \
    -ex 'break __libc_start_main' \
    -ex run \
    -ex 'bt' \
    -ex 'info args' \
    ./hello_cdyn

Backtrace-এ দেখবেন main-এর নিচে __libc_start_main — caller-এর চেইনে runtime-টা বসে আছে, আর info args-এ main-এর pointer-টাও দৃশ্যমান (runtime-ই তাকে ডাকছে)।

যা প্রমাণিত: “hello world”-ও runtime-free না — ভাষা বদলালে আপনার প্রথম লাইন চলার আগে ৬ থেকে ৭০০ syscall পর্যন্ত পার্থক্য, আর প্রতিটা syscall-ই ওই ভাষার runtime-এর কোনো না কোনো সেবা (linker, scheduler, GC, loader)-র জন্মসাক্ষ্য।

এটা কী প্রমাণ করে

main-এর আগে runtime বাস্তবেই syscall করে — আর কোন ভাষার runtime কতটা ভারী তা syscall-count আর type-distribution দিয়ে সরাসরি মাপা যায়: C-static নগ্ন, Go নিজের scheduler/GC সাজায়, Java/Python dynamic-linking আর VM-setup-এ ডোবে।

নিজে বানান

BUILD IT

-nostdlib — libc ছাড়া পৃথিবীতে হ্যালো বলা

C + inline asm (x86-64 Linux) · ●●●○○
  1. _start লেখা — linker-এর default entry নিজে সরবরাহ
  2. System V startup ABI মেনে argc/argv stack থেকে পড়া
  3. inline asm দিয়ে write(1, buf, len) syscall
  4. exit(code) syscall — না ডাকলে কী হয় দেখা
  5. echo $? দিয়ে exit code যাচাই

libc সরালে যা যা হারালেন: printf (buffering সহ), exit (atexit chain), crt0 (argument unpacking)। বাকি থাকলে শুধু kernel-এর raw syscall interface। তাহলে সেটাই লিখি — <Term id="system-call">System Call</Term> স্তরে, একদম নিচে:

/* bare.c — no libc, no crt0: শুধু আমরা আর kernel */
/* compile: gcc -static -nostdlib -nostartfiles -o bare bare.c */

static long sys(long n, long a, long b, long c) {
    long ret;
    __asm__ volatile ("syscall"
        : "=a"(ret)                  /* rax: ফেরত/errno */
        : "a"(n), "D"(a), "S"(b), "d"(c)   /* rax=nr, rdi, rsi, rdx */
        : "rcx", "r11", "memory");   /* syscall এই দুই register নষ্ট করে */
    return ret;
}

#define SYS_write 1
#define SYS_exit  60

void _start(void) {
    /* System V process-init ABI: [rsp]=argc, [rsp+8]=argv[0], ... */
    long argc;
    __asm__ volatile ("movq %%rsp, %0" : "=r"(argc)); /* rsp নিজেই argc! */

    char msg[] = "hello from the metal\n";
    long len = sizeof(msg) - 1;

    /* argc আর argv[0]-এর length print করি — প্রমাণ ABI পড়তে পারছি */
    char hdr[32];
    int i = 0;
    hdr[i++] = 'a'; hdr[i++] = 'r'; hdr[i++] = 'g'; hdr[i++] = 'c';
    hdr[i++] = ':';
    hdr[i++] = (char)('0' + argc);
    hdr[i++] = '\n';
    sys(SYS_write, 1, (long)hdr, i);

    sys(SYS_write, 1, (long)msg, len);
    sys(SYS_exit, 42, 0, 0);          /* 42 ফেলে দিই — বাইরে যাচাই করব */

    __builtin_unreachable();          /* exit ফেরত আসে না */
}
gcc -static -nostdlib -nostartfiles -o bare bare.c
./bare
echo $?                            # 42 — আমাদের exit code, kernel পর্যন্ত পৌঁছেছে
file bare | sed 's/,.*//'          # statically linked, খুব ছোট
ls -la bare                        # ~9 KB (default gcc hello: 800KB+ dynamic)
argc:1
hello from the metal
42

যা ঘটল: kernel execve করে e_entry-তে (আমাদের _start) jump করল। Stack-এর শীর্ষে বৈধ ABI অনুযায়ী argc ছিল — আমরা সেটা পড়লাম (./bare চালাতেই 2)। দুইটা syscall দিয়ে লিখে বেরিয়ে গেলাম। কোনো runtime নেই — মানে কোনো সেবাও নেই: buffer নেই (প্রতি write সত্যিকারের context switch), exit-handler নেই, malloc নেই, এমনকি stack overflow-ও কেউ ধরবে না — প্রথম fault-এই SIGSEGV।

নিজে বাড়ান:

  1. SYS_exit call-টা মুছে দিন — _start “ফেরত” আসবে garbage address-এ → SIGSEGV। Crash message-টা পড়ে বোঝান কেন: ret-এর ঠিকানা হিসেবে stack-এ যা ছিল তা-ই jump।
  2. argv[0] string-টা print করুন (stack walk: [rsp+8] = pointer to argv[0]) — আপনার প্রথম নিজস্ব ABI parser।
  3. arm64-তে syscall convention ভিন্ন: mov w8, #64; svc #0 (write), arguments x0–x2। একটা ARM Mac/Docker-এ port করে দেখুন — <Term id="abi">ABI</Term> বদলালে runtime-বিহীন কোডে কোন সব জিনিস ভাঙে তা হাতে-কলমে।
  4. ltrace/strace ./bare চালান — মোট syscall সংখ্যা আগের experiment-এর C-static-এর চেয়েও কম হওয়ার কথা (write×2 + exit)।

বাস্তব সিস্টেমে

Production-এ runtime-চিন্তা কোথায় দেখা যায়

Container images। FROM scratch + static Go binary = একটা layer, কোনো libc নেই — সম্ভব কারণ Go-র runtime প্রায় সব syscall নিজেই জানে। আর Java-র official image কেন শত-SHA-MB? Class loader, JIT, GC — পুরো runtime-ই artifact। Image-size debate আসলে runtime-design debate।

Serverless cold start। Lambda-র প্রথম invocation-এ যে latency — execve, runtime init, class loading (JVM-এ শত-ms), interpreter boot (Python-এ দশ-ms), pool warmup — পুরোটাই startup-chain-এর দাম। SnapStart/provisioned concurrency আসলে এই chain precompute।

Embedded আর kernel। Freestanding environment (no hosted libc) — firmware, kernel modules, Rust #![no_std] — এখানে runtime-এর সেবাগুলো আপনি explicit কিনতে/লিখতে হয়, ঠিক আজকের build যেমন করেছে। Zig-এর “no hidden control flow” slogan এই দর্শনের marketing রূপ।

Observability। Go-র pprof profile-এ runtime.mcall, morestack, gcBgMarkWorker frames দেখবেন — আপনার কোডের পাশে runtime-এর খরচও এখানেই। JVM-এ JIT compilation threads, GC threads আলাদা দেখা যায়। Runtime-কে চিনলে flame graph-এর অচেনা frames-গুলোও নাম-ধরে পড়া যায়।

Crash forensics। Segfault log-এ libc_start_call_main+0x7f জাতীয় frame, Java hs_err file-এ “Current thread” + loader info, Go panic dump-এ goroutine stack — production incident-এর প্রথম page-ই runtime-এর আত্মবিবরণ। এটা পড়তে না পারলে debugging অন্ধের মতো।

Interop contracts। JNI, cgo, PyO3, NAPI — প্রতিটা framework-এর documentation-এর প্রথম warning-ই আজকের hood-এর FFI section: দুই runtime-এর মধ্যে object/GC/threading assumptions পার হওয়া মানে সতর্ক চুক্তি। Node-এর event loop-এ blocking call, Go-র goroutine-এ C callback — ক্লাসিক production footgun, দুটোই boundary-violation।

যে ভুলগুলো সবাই করে

“C-এর কোনো runtime নেই।”

ছোট আছে, শূন্য না। crt0/_start, __libc_start_main-এর init sequence, malloc-family, stdio buffers, TLS, atexit — সবই runtime service। “No runtime” বাক্যটার সঠিক রূপ: C-এর runtime এতই পাতলা যে আপনি ভুলে যেতে পারেন — আর আজকের build দেখিয়েছে ভুলে গেলে কী খেসারৎ (exit ডাকতেই হয়, নইলে SIGSEGV)।

“main() হলো program-এর entry point।”

ELF-এর e_entry-ই entry — সেখান থেকে _start → __libc_start_main চেইন পেরিয়ে আসে main। Constructors (.init_array) main-এর আগে চলে; C++ static object-দের constructor-ও। তাই “main-এ breakpoint বসালেই সব শুরু দেখব” — মাঝামাঝি ভুল; আগের ঘটনাগুলো মিস।

“Goroutine মানে lightweight OS thread।”

Goroutine OS thread না-ই — M:N multiplexed user-space construct (G), যেটা P-র license-সহ M (OS thread)-এর উপর চলে। Blocking syscall-এ M আলাদা হয়ে যায়, P অন্য M-এ migrate করে — এই handoff-টাই Go server-কে লক্ষ-lakhs connection-এ scale করায়। Thread-model-এ ভাবলে goroutine-per-request design-এর economics-ই বোঝা যাবে না।

“-nostdlib দিলে binary এখন OS-mukto বা pure।”

উল্টো — এখন আপনি OS-এর সাথে সরাসরি, raw ABI-তে কথা বলছেন। libc ছিল সেই negotiation-এর translator। আমাদের bare.c এখনও write/exit syscall ডাকে — kernel ছাড়া এটাও চলবে না। Pure হতে হলে freestanding target (embedded, no OS) লাগবে — তখন hardware-এর interrupt vector-ই আপনার syscall interface।

বুঝেছেন কি না দেখুন

1

execve থেকে main পর্যন্ত চেইনটা order অনুযায়ী লিখুন (kernel-side সহ), আর বলুন .init_array ঠিক কোন ধাপে চলে — main-এর আগে না পরে?

স্মরণ

execve → kernel: ELF load/map + interpreter(ld.so) + stack(argc/argv/envp/auxv) → _start (crt0: unpack args, TLS early setup) → __libc_start_main (thread infra, atexit register) → .init_array → main(argc, argv, envp) → exit() → .fini_array/atexits → exit_group।

.init_array চলে main-এর আগে — __libc_start_main-এর ভেতরে, main call-এর ঠিক আগ মুহূর্তে।

2

Go-র goroutine stack ২ KB দিয়ে শুরু হয়ে বাড়তে পারে, OS thread-এর stack fixed ৮ MB reservation। দুই design-এর trade-off কী, আর Go কোন mechanism-এ growth detect করে?

যুক্তি

OS-thread: ৮ MB virtual reserve প্রতি thread — হাজার thread মানে ৮ GB virtual (physical না, তবু address-space pressure + commit-on-touch), আর shrink হয় না। Goroutine: ২ KB physical-scale start — million goroutine viable, কিন্তু বাড়ার সময় copy-cost + pointer-adjust complexity।

Detection: compiler প্রতিটা function prologue-এ stack-guard compare বসায় (SP vs stack-এর bound); ছুঁয়ে ফেললে runtime-এর morestack ডাকে — বড় stack alloc করে old contents copy, তারপর ফেরত। Precise stack maps থাকায় pointer adjust নিরাপদ।

3

strace-এ আপনার Go binary-তে sched_getaffinity আর sigaltstack দেখলেন। প্রতিটা কোন runtime-feature-এর সেবা করে?

প্রয়োগ

sched_getaffinity — GOMAXPROCS auto-detection: runtime জিজ্ঞেস করে কয়টা CPU-তে চলার অনুমতি (container quota সহ), ততগুলো P তৈরি করে — scheduler-এর sizing।

sigaltstack — signal-গুলো বিশেষ alternate stack-এ deliver করার ব্যবস্থা; সবচেয়ে জরুরি SIGSEGV-এর জন্য: stack overflow-এ normal stack অবৈধ হয়ে যায়, handler ওখানে চলতে পারে না। Go এই signal-টা ধরেই stack-growth fault বনাম real segfault আলাদা করে (morestack machinery)। অর্থাৎ দুটো syscall-ই আপনার কোড না — scheduler আর stack-manager-এর জন্মকাগজ।

4

আপনার team JVM microservice-কে Lambda-তে নিয়ে যেতে চায়; cold start budget 500 ms। Runtime-knowledge ব্যবহার করে চারটা concrete mitigation suggest করুন এবং প্রতিটা কোন startup-ধাপ ছোট করে ব্যাখ্যা করুন।

ডিজাইন

১. GraalVM native image / AOT — JIT-tier warming (interpret→C1→C2) ধাপটাই বাদ: আগে থেকে machine code, class-loading সরলীকৃত। Cold-start-এর সবচেয়ে বড় কাট। ২. jlink custom runtime + AppCDS/AOT cache — class loader-এর lazy-load storm আগেই computed archive-এ; openat/stat flood কমে (experiment-এ যে linking-cost দেখেছিলেন)। ৩. Provisioned concurrency / SnapStart — execve+runtime-init ধাপটা request-path থেকে সরে যায় (pre-warmed sandbox restore/fork)। ৪. Heap/metaspace reserve ছোট করা (-Xmx, -XX:MaxMetaspaceSize) — kernel-এর বিশাল mmap reservations (experiment trace-এ দেখা) ছোট হয়, setup fast-path।

Trade-off মনে রাখুন: AOT-এ JIT peak-throughput হারায় — warm traffic-এ বাণিজ্য। সঠিক metric: cold p99 আর warm throughput দুটোই।

5

আজকের bare.c-তে যদি sys(SYS_exit,...) না থাকত, প্রোগ্রাম ঠিক কীভাবে মরত? আর একই কোড C-static-এ (libc সহ) crash করত না — কেন?

যুক্তি

_start একটা normal function-এর মতো ret execute করবে — কিন্তু তাকে ডাকেনি কেউ: return address-এর জায়গায় stack-এ যা আছে (argc-এর নিচের slot/garbage) সেটাই PC হয়ে যায় → invalid address-এ jump → SIGSEGV। Hosted environment-এ crt0-র _start ইচ্ছাকৃতভাবে exit(main(...)) করে — ফেরার রাস্তাই নেই।

libc-সহ কোডে crash হতো না কারণ main return করলে __libc_start_main ধরে exit(ret) ডাকে — runtime-টা ঠিক এই “function যাকে ডাকেনি তার কাছে ফেরা যায় না” সমস্যাটাই সমাধান করে রেখেছে।

এরপর কী

এরপর কী

Runtime-এর মঞ্চ এখন আপনার চেনা — startup chain, scheduler, GC-integration, syscall boundary। এবার সেই মঞ্চে ভাষাতত্ত্বের সবচেয়ে সুন্দর অভিনেতাটাকে ডাকা যাক: closure। গত কয়েক লেসনের সব স্তর এখানে এসে মেলে — formal-semantics-এর environment model, লেসন ৯-এর heap allocation (escaping closure-এর environment হঠাৎ heap-এ বাস করে!), runtime-এর function-call machinery। Closure আসলে কী — ফাংশন না ডেটা? Captured variable-টা copy না reference? Python-এর loop-bug কেন হয়, JS-এর var/let পার্থক্য machine level-এ কোথায়, আর C++-এর [=]/[&] capture কীভাবে desugar হয় — তারপর নিজের হাতে formal-semantics-এর evaluator-টাকে lambda-capable, closure-correct interpreter বানানো। পরের লেসনে।

পরের লেসন: Closure — ফাংশন যখন environment সাথে নিয়ে হাঁটে।

আরও পড়ুন

  • Linkers and Loaders — John R. Levine · Startup chain-এর পূর্বকথা — executable থেকে _start-এ পৌঁছানো পর্যন্ত loader-এর কাজ
  • Schedulers in Go (work-stealing, G-M-P) — Dmitry Vyukov (Go runtime design), golang.org/src/runtime/proc.go comments · Go scheduler-এর G-M-P design ও preemption evolution-এর primary source
  • CPython Internals / ceval.c — Anthony Shaw (book), Python Software Foundation (source) · Bytecode evaluation loop আর GIL-এর বাস্তব বাস্তবায়ন
  • System V Application Binary Interface, x86-64 supplement — Michael Matz, Jan Hubička, Andreas Jaeger, Mark Mitchell · Process initialization contract — _start-এর entry state ঠিক করে যে document