================================================================================
AXIOMATIC WORKSHEET: WHAT HAPPENS WHEN open() IS CALLED
================================================================================
Each step: DO something, SEE output, DERIVE knowledge, ASK why.
No magic. No new things without derivation.

================================================================================
STEP 01: THE SOURCE CODE
================================================================================

DO:   View your source file.
RUN:  cat minimal_open.c

You will see:
    #include <fcntl.h>
    int main() {
        int fd = open("somefile", O_RDWR);
        return 0;
    }

OBSERVE:
    Line 1: #include <fcntl.h>
    Line 3: open("somefile", O_RDWR)

INQUIRY:
    Q1: What is O_RDWR? A number? A string? Magic?
    Q2: What is fcntl.h? Where is it?
    Q3: What is open()? Where is it defined?

AXIOM: At this stage, we have TEXT. Nothing more.


================================================================================
STEP 02: FIND THE HEADER
================================================================================

DO:   Find where O_RDWR is defined.
RUN:  grep -r "define O_RDWR" /usr/include | head -1

OUTPUT: /usr/include/asm-generic/fcntl.h:#define O_RDWR 00000002

OBSERVE:
    O_RDWR = 00000002
    The leading 0 means OCTAL (base 8).

MATH (Octal to Decimal):
    00000002 in octal
    = 0*8^7 + 0*8^6 + 0*8^5 + 0*8^4 + 0*8^3 + 0*8^2 + 0*8^1 + 2*8^0
    = 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2
    = 2

CONCLUSION: O_RDWR = 2 (decimal)

INQUIRY:
    Q: Why octal? A: Historical. Unix permissions use octal (chmod 755).


================================================================================
STEP 03: PREPROCESSING
================================================================================

DO:   Run the preprocessor to see what the compiler actually gets.
RUN:  gcc -E minimal_open.c | tail -5

OUTPUT (filtered):
    int fd = open("somefile", 02);

OBSERVE:
    O_RDWR has been REPLACED with 02.
    The preprocessor does TEXT SUBSTITUTION only.

MATH (Verification):
    We predicted O_RDWR = 2.
    Preprocessor output shows 02 (octal) = 2 (decimal).
    VERIFIED.

AXIOM: Preprocessor replaces macros with their values. No code generated yet.


================================================================================
STEP 04: COMPILATION TO OBJECT FILE
================================================================================

DO:   Compile to object file (not executable).
RUN:  gcc -c minimal_open.c -o minimal_open.o

DO:   Examine the call instruction.
RUN:  objdump -d minimal_open.o | grep -A2 "call"

OUTPUT:
    20:   e8 00 00 00 00    call   25 <main+0x25>

OBSERVE:
    Address 0x20: opcode e8 (CALL instruction)
    Address 0x21: operand 00 00 00 00 (ZEROS!)

INQUIRY:
    Q: Why are there ZEROS where the address should be?
    A: The compiler does not know where open() will be.
       open() is in libc, which is not linked yet.

MATH (Instruction Layout):
    e8 = opcode (1 byte)
    00 00 00 00 = operand (4 bytes, displacement)
    Total = 5 bytes
    Next instruction at 0x20 + 5 = 0x25

AXIOM: Object file has PLACEHOLDERS (zeros) for external functions.


================================================================================
STEP 05: RELOCATION ENTRIES
================================================================================

DO:   Find the relocation entry that tells the linker what to fix.
RUN:  readelf -r minimal_open.o | grep open

OUTPUT:
    000000000021  000500000004 R_X86_64_PLT32    0000000000000000 open - 4

OBSERVE:
    Offset = 0x21 (where to patch)
    Type = R_X86_64_PLT32 (patch with PLT address)
    Symbol = open (what to find)
    Addend = -4 (subtract 4 from the result)

INQUIRY:
    Q: Why offset 0x21, not 0x20?
    A: 0x20 is the opcode (e8). 0x21 is the operand (the zeros to fill).

    Q: Why addend -4?
    A: The CPU calculates: target = RIP + displacement.
       RIP points to NEXT instruction (0x25).
       Patch location is 0x21.
       RIP = 0x21 + 4 = 0x25.
       So displacement must account for this: subtract 4.

MATH (Addend Derivation):
    Patch location = 0x21
    Next RIP after fetch = 0x21 + 4 = 0x25
    Difference = 4
    Therefore addend = -4

AXIOM: Relocation entry = {offset, type, symbol, addend}. It is a FORMULA.


================================================================================
STEP 06: LINKING - SECTION LAYOUT
================================================================================

DO:   Link the object file to create an executable.
RUN:  gcc minimal_open.o -o minimal_open

DO:   Examine the section layout.
RUN:  readelf -S minimal_open | grep -E "(plt|got|text)"

OUTPUT:
    [13] .plt      PROGBITS  0000000000001020
    [14] .plt.got  PROGBITS  0000000000001040
    [15] .plt.sec  PROGBITS  0000000000001050
    [16] .text     PROGBITS  0000000000001060
    [23] .got      PROGBITS  0000000000003fb8

OBSERVE:
    .plt     at 0x1020   (Procedure Linkage Table - CODE)
    .plt.sec at 0x1050   (PLT Security section - CODE)
    .text    at 0x1060   (Your code - CODE)
    .got     at 0x3fb8   (Global Offset Table - DATA)

INQUIRY:
    Q: PLT comes BEFORE GOT in memory. Why?
    A: Code sections (.plt, .text) are grouped together.
       Data sections (.got) are grouped separately.
       This is for memory protection (code = read+execute, data = read+write).

MATH (Address Spacing & Patterns):
    Pattern 1: The PLT Header Size
    .plt start (0x1020) vs .plt.got start (0x1040).
    Gap = 0x1040 - 0x1020 = 0x20 (32 bytes).
    
    Pattern 2: The PLT Entry Size (Standard)
    .plt.sec start (0x1050) vs .text start (0x1060).
    Gap = 0x1060 - 0x1050 = 0x10 (16 bytes).

    EXERCISE:
    If we had a second function 'close()', where would its PLT stub be?
    Answer: 0x1050 + 0x10 = 0x1060 (Pushing .text down).
    
    Verify this spacing on YOUR machine! It is a rigid pattern.

AXIOM: Linker creates PLT (code) and GOT (data). PLT is at lower address.


================================================================================
STEP 07: WHAT IS PLT?
================================================================================

PLT = Procedure Linkage Table.

DO:   Look at the PLT entry for open.
RUN:  objdump -d minimal_open | grep -A3 "open@plt"

OUTPUT:
    0000000000001050 <open@plt>:
    1050:   f3 0f 1e fa       endbr64
    1054:   ff 25 76 2f 00 00 jmp    *0x2f76(%rip)

OBSERVE:
    Address 0x1050: open@plt
    Instruction at 0x1054: jmp *0x2f76(%rip)
    This is an INDIRECT jump - jump to address STORED somewhere.

INQUIRY:
    Q: What is 0x2f76(%rip)?
    A: RIP-relative addressing. Add 0x2f76 to the next RIP.

    Q: Why use RIP-relative?
    A: It allows the code to be generally position-independent (within limits).
       The *distance* between PLT (code) and GOT (data) is constant.
       Even if kernel moves the whole program, the distance remains the same.

MATH (Calculate Jump Target):
    Instruction at 0x1054, length = 6 bytes (ff 25 + 4 bytes displacement)
    Next RIP = 0x1054 + 6 = 0x105a
    Target = 0x105a + 0x2f76 = _____

    0x105a in decimal: 4186
    0x2f76 in decimal: 12150
    Sum: 4186 + 12150 = 16336
    16336 in hex: 0x3fd0

    EXERCISE:
    If the displacement was 0x2f86 instead of 0x2f76, where would it jump?
    Target = 0x105a + 0x2f86 = 0x3fe0 (Next GOT slot!)
    This confirms the pattern: 16 bytes code gap aligns with 8 bytes data gap?
    (Wait for Step 08 to see!)

CONCLUSION: PLT jumps to address stored at 0x3fd0.

AXIOM: PLT is CODE. It jumps to an address stored in GOT.


================================================================================
STEP 08: WHAT IS GOT?
================================================================================

GOT = Global Offset Table.

DO:   Find the GOT entry for open.
RUN:  readelf -r minimal_open | grep "open"

OUTPUT:
    000000003fd0  000400000007 R_X86_64_JUMP_SLO 0000000000000000 open@GLIBC

OBSERVE:
    Address 0x3fd0 is the GOT slot for open.
    This matches our calculation in STEP 07!

DO:   Read the current value in GOT.
RUN:  objdump -s -j .got minimal_open | grep 3fd0

OUTPUT:
    3fd0 30100000 00000000

OBSERVE:
    Value at 0x3fd0 = 0x0000000000001030 (little-endian)

INQUIRY:
    Q: What is 0x1030?
    A: Let's find out in the next step.

MATH (Little-Endian to Value):
    Bytes: 30 10 00 00 00 00 00 00
    Read right-to-left: 00 00 00 00 00 00 10 30
    Result: 0x1030

MATH (The Index Pattern):
    GOT Base = 0x3fb8 (from readelf -S output in Step 06)
    Reserved Slots = 3 (Always!)
    Slot Size = 8 bytes
    Our Address = 0x3fd0

    Gap = 0x3fd0 - 0x3fb8 = 0x18 (24 decimal)
    Index = Gap / 8 = 3.
    
    Why 3?
    Index 0: Reserved
    Index 1: Reserved
    Index 2: Reserved
    Index 3: First Function (open)!

    EXERCISE:
    Where would the SECOND function be located?
    Index 4.
    Addr = 0x3fb8 + (4 * 8) = 0x3fb8 + 32 = 0x3fb8 + 0x20 = 0x3fd8.

AXIOM: GOT is DATA. It stores addresses. Initially points to resolver stub.


================================================================================
STEP 09: THE RESOLVER STUB
================================================================================

DO:   Find what is at 0x1030.
RUN:  objdump -d minimal_open | grep -A3 "1030:"

OUTPUT:
    1030:   f3 0f 1e fa         endbr64
    1034:   68 00 00 00 00      push   $0x0
    1039:   f2 e9 e1 ff ff ff   jmp    1020 <_init+0x20>

OBSERVE:
    0x1030 is a STUB in .plt section.
    It pushes 0 (index) and jumps to 0x1020 (resolver).

    DEFINITION: What is a STUB?
    A tiny piece of code (glue) that handles the transition between two bigger pieces.
    Here: It is the "glue" that pauses the program to call the linker.

INQUIRY:
    Q: Why push 0?
    A: Index 0 means "first function in relocation table" = open.

    Q: What is at 0x1020?
    A: The PLT header - calls the dynamic linker resolver.

MATH (The Lazy Binding Loop):
    1. Code calls 0x1050 (open@plt)
    2. 0x1050 jumps to *0x3fd0
    3. 0x3fd0 contains 0x1030 (The Stub) - LOOP BACK!
    4. 0x1030 pushes index, jumps to 0x1020 (The Manager)
    5. 0x1020 calls resolver
    6. Resolver finds real open() in libc
    7. Resolver writes real address to 0x3fd0
    8. Next call goes directly to libc (fast!)

    MATH (The Loop Pattern):
    Fast Path: 1050 -> 3fd0 -> Libc (Forward)
    Slow Path: 1050 -> 3fd0 -> 1030 (Backward!!)
    
    EXERCISE:
    Check the address direction.
    1050 > 1030. The jump goes BACKWARDS in memory initially.
    1050 < Libc. The jump goes FORWARDS in memory eventually.

AXIOM: First call is SLOW (resolver). Subsequent calls are FAST (direct).


================================================================================
STEP 10: THE CALL INSTRUCTION PATCH
================================================================================

DO:   Find the patched call instruction.
RUN:  objdump -d minimal_open | grep "call.*open"

OUTPUT:
    1169:   e8 e2 fe ff ff      call   1050 <open@plt>

OBSERVE:
    Address 0x1169: the call instruction
    Operand: e2 fe ff ff (was 00 00 00 00 in object file!)
    Target: 0x1050 (open@plt)

    DEFINITION: What is a PATCH?
    Modifying the binary code after it has been generated.
    The Linker overwrote the "00 00 00 00" placeholder with a real calculated value.

MATH (Verify the Displacement):
    Displacement bytes: e2 fe ff ff (little-endian)
    As 32-bit signed: 0xfffffee2

    Is this negative? Check sign bit.
    0xfffffee2 in binary starts with 1... so YES, negative.

    Two's Complement to Decimal:
    Invert:  0x0000011d
    Add 1:   0x0000011e
    0x11e = 286 decimal
    So displacement = -286

    Verify: next_RIP + displacement = target
    next_RIP = 0x1169 + 5 = 0x116e
    0x116e + (-286) = 0x116e - 0x11e = 0x1050

    CORRECT!

    MATH (The Direction Pattern):
    Call is at 0x1169.
    Target is at 0x1050.
    1169 > 1050.
    Therefore, the call must jump BACKWARDS (Negative displacement).
    
    EXERCISE:
    If the function was at 0x2000, would displacement be positive or negative?
    0x2000 > 0x116e.
    Target > RIP.
    Result would be POSITIVE.

    AXIOM: Linker computed displacement = target - next_RIP = 0x1050 - 0x116e = -286.

AXIOM: Linker computed displacement = target - next_RIP = 0x1050 - 0x116e = -286.


================================================================================
STEP 11: RUNTIME - LIBC LOADING
================================================================================

DO:   Run the program under GDB and check GOT before/after.
RUN:  gdb -batch -ex "break main" -ex "run" -ex "x/gx 0x3fd0" ./minimal_open

    EXPLANATION (GDB Flags):
    -batch: Run in non-interactive mode (script mode).
    -ex "cmd": Execute this GDB command.
    break main: Stop when main() starts.
    run: Start the program.
    x/gx 0x3fd0: eXamine memory, G (Giant/8-bytes), X (Hex).

OUTPUT (before first call):
    0x3fd0: 0x0000555555555030

OBSERVE:
    GOT still points to stub (now with ASLR base added).

DO:   Step past the open() call and check GOT again.
RUN:  gdb -batch -ex "break *main+50" -ex "run" -ex "x/gx 0x3fd0" ./minimal_open

OUTPUT (after first call):
    0x3fd0: 0x00007ffff7d1b150

OBSERVE:
    GOT now points to LIBC! The resolver patched it.

MATH (ASLR Address Calculation):
    libc base (example): 0x7ffff7c00000
    open offset in libc: 0x11b150
    open runtime addr:   0x7ffff7c00000 + 0x11b150 = 0x7ffff7d1b150

    MATH (The Pattern):
    Every single function in libc is at:
    [Random Base Address] + [Static File Offset]

    EXERCISE:
    If libc loaded at 0x400000000000.
    And open is at offset 0x11b150.
    Where is open in memory?
    Answer: 0x40000011b150.

DO:   Verify the offset.
RUN:  readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep " open@"

OUTPUT:
    000000000011b150 ... open@@GLIBC_2.2.5

CONFIRMED: Offset 0x11b150 matches!

AXIOM: Runtime address = ASLR_base + fixed_offset.


================================================================================
STEP 12: WEAK SYMBOLS - open IS AN ALIAS
================================================================================

DO:   Find all symbols at the same address as open.
RUN:  readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep 11b150

OUTPUT:
    000000000011b150 ... FUNC ... __open_nocancel
    000000000011b150 ... FUNC ... open@@GLIBC_2.2.5
    000000000011b150 ... FUNC ... __libc_open64

OBSERVE:
    THREE symbols at the SAME address (0x11b150).
    They are ALIASES.

DO:   Check if open is a weak symbol.
RUN:  nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " open@"

OUTPUT:
    000000000011b150 W open@@GLIBC_2.2.5

OBSERVE:
    W = WEAK symbol.
    Meaning: Can be overridden by user-defined symbol.

INQUIRY:
    Q: Why weak?
    A: So you can write your OWN open() and libc will use yours.
       Example: LD_PRELOAD tricks for debugging or interception.

    EXPLANATION (The Override Pattern):
    Strong Symbol (Your code) > Weak Symbol (Libc).
    If you define 'int open(...)', the linker picks YOURS.
    
    EXERCISE:
    If you wrote a function called 'read' in your C file.
    Would libc use your 'read' or its own?
    Answer: Yours! (Because libc symbols are weak).

AXIOM: open = __open_nocancel = __libc_open64 (same address, different names).


================================================================================
STEP 13: LIBC ENTRY - endbr64 SECURITY
================================================================================

DO:   Disassemble the first instruction of open.
RUN:  gdb -batch -ex "x/1i 0x7ffff7d1b150" ./minimal_open

OUTPUT:
    0x7ffff7d1b150:  endbr64

OBSERVE:
    First instruction is NOT push rbp.
    It is endbr64 (f3 0f 1e fa).

INQUIRY:
    Q: What is endbr64?
    A: Intel CET (Control-flow Enforcement Technology).
       "End Branch 64-bit" - marks VALID jump targets.

    Q: Why is this here?
    A: If malicious code jumps to middle of a function, CPU will crash.
       This protects against ROP (Return Oriented Programming) attacks.

    MATH (The NOP Pattern):
    Opcode: f3 0f 1e fa (4 bytes).
    On older CPUs: This executes as a NOP (No Operation).
    On newer CPUs: It checks for valid branching.
    
    EXERCISE:
    Why use an instruction that does nothing on old CPUs?
    Answer: Backward Compatibility! Old CPUs don't crash, they just ignore it.

AXIOM: endbr64 marks the start of a valid function entry point.


================================================================================
STEP 14: FUNCTION PROLOGUE - STACK SETUP
================================================================================

DO:   View the next few instructions.
RUN:  gdb -batch -ex "x/10i 0x7ffff7d1b150" ./minimal_open

OUTPUT:
    endbr64
    push   %rbp           ; Save caller's base pointer
    mov    %rsp,%rbp      ; Set up new frame
    push   %r12           ; Save callee-saved registers
    push   %rbx
    sub    $0x60,%rsp     ; Reserve 96 bytes on stack

OBSERVE:
    Standard function prologue.
    Reserves 0x60 = 96 bytes for local variables.

MATH (Stack Frame Size):
    0x60 in hex
    = 6*16 + 0*1
    = 96 bytes

    MATH (The Growth Pattern):
    Instruction: sub $0x60, %rsp
    Subtraction means the stack grows DOWN (towards lower addresses).
    
    EXERCISE:
    If a function needed 200 bytes of local variables.
    What hex value would follow 'sub'?
    200 / 16 = 12 remainder 8. (Wait, 200 = 12*16 + 8).
    12 = 0xC.
    So 0xC8?
    Check: 12*16 + 8 = 192 + 8 = 200.
    Answer: sub $0xc8, %rsp.

INQUIRY:
    Q: Why save r12 and rbx?
    A: Callee-saved registers. Function must restore them before returning.

AXIOM: Function prologue establishes stack frame and saves registers.


================================================================================
STEP 15: STACK CANARY - BUFFER OVERFLOW DETECTION
================================================================================

DO:   Find the canary instruction.
RUN:  gdb -batch -ex "x/15i 0x7ffff7d1b150" ./minimal_open | grep fs

    EXPLANATION (GDB Flags):
    x/15i: eXamine 15 Instructions (Opcode listing).
    grep fs: Only show lines containing "fs" register.

OUTPUT:
    mov    %fs:0x28,%rax
    mov    %rax,-0x48(%rbp)

OBSERVE:
    %fs:0x28 = Thread Local Storage, offset 0x28 (40 bytes).
    A SECRET RANDOM NUMBER is read and stored on stack.

INQUIRY:
    Q: What is %fs?
    A: Segment register pointing to Thread Local Storage (TLS).
       In modern Linux, it points to the TCB (Thread Control Block).

    Q: What is at offset 0x28?
    A: The stack canary - a random value set at program start.

    Q: Why store it on the stack?
    A: Before returning, function checks: Did this value change?
       If YES = Buffer Overflow detected = CRASH.

MATH (Canary Location):
    Base pointer: rbp
    Canary stored at: rbp - 0x48
    0x48 = 72 bytes below rbp

    MATH (The Buffer Pattern):
    Variables = rbp - 0x48 to rbp (72 bytes).
    If a variable writes 73 bytes, it overwrites the Canary.
    
    EXERCISE:
    If you declare 'char buf[80]', where does it go?
    It needs 80 bytes. Canary is at 72.
    Compiler must grow the stack frame!
    (See Step 14 - sub $0x60 is not enough, it would increase).

AXIOM: Stack canary detects buffer overflows at function return.


================================================================================
STEP 16: THREAD SAFETY CHECK
================================================================================

DO:   Find the threading check.
RUN:  gdb -batch -ex "x/20i 0x7ffff7d1b150" ./minimal_open | grep single

OUTPUT:
    cmpb   $0x0,0xefeae(%rip)   ; __libc_single_threaded

OBSERVE:
    Compares __libc_single_threaded variable with 0.

INQUIRY:
    Q: What is __libc_single_threaded?
    A: A global variable in libc.
       1 = Only one thread exists (fast path).
       0 = Multiple threads exist (slow path with locks).

    Q: Why two paths?
    A: Fast path avoids lock overhead.
       Slow path enables thread cancellation.

    MATH (The Logic Pattern):
    Instruction: cmpb $0x0, 0xefeae(%rip)
    Logic: if (variable != 0) -> Fast.
    Logic: if (variable == 0) -> Slow. (Wait, non-zero usually means TRUE?)
    Here: 'Single Threaded' is TRUE (1) -> Optimized.
    
    EXERCISE:
    If your program spawns a thread (`pthread_create`).
    What happens to this variable?
    Answer: libc sets it to 0 immediately!
    The whole program switches to "Safe Mode" (Slow).

AXIOM: Single-threaded programs take the fast path (no locks).


================================================================================
STEP 17: O_CREAT CHECK - BITWISE OPERATION
================================================================================

DO:   Find the O_CREAT check.
RUN:  grep -r "O_CREAT" /usr/include/asm-generic/fcntl.h | head -1

OUTPUT:
    #define O_CREAT 00000100

MATH (Octal to Hex):
    Octal 100 = 1*64 + 0*8 + 0*1 = 64 decimal
    64 decimal = 0x40 hex

DO:   Find the AND instruction in libc.
RUN:  gdb -batch -ex "x/30i 0x7ffff7d1b150" ./minimal_open | grep "and.*0x40"

OUTPUT:
    and    $0x40,%r10d

OBSERVE:
    Checks if O_CREAT bit is set in flags.

MATH (Bitwise AND):
    Your flags: O_RDWR = 2 = 0000 0010 binary
    O_CREAT mask:      0x40 = 0100 0000 binary
    AND result:              0000 0000 = 0

    Result is 0, so O_CREAT is NOT set.
    No third argument (mode) is needed.

    MATH (The Logic Pattern):
    AND instruction acts as a FILTER using a MASK.
    Mask: 0x40 (Only bit 6 is 1).
    Input: O_RDWR (0x02).
    Result: 0.

    EXERCISE:
    If you passed `O_CREAT | O_RDWR` (0x42).
    0x42 = 0100 0010.
    AND 0x40 (0100 0000).
    Result = 0100 0000 (0x40).
    Non-zero result -> Libc reads the 3rd argument!

INQUIRY:
    Q: Why check O_CREAT?
    A: If set, open() needs a THIRD argument (file permissions).
       Without O_CREAT, only 2 arguments are needed.

AXIOM: O_CREAT (0x40) determines if mode argument is required.


================================================================================
STEP 18: REGISTER SHUFFLE - open() TO openat()
================================================================================

DO:   Find the register setup before syscall.
RUN:  gdb -batch -ex "x/5i 0x7ffff7d1b199" ./minimal_open

OUTPUT:
    mov    $0xffffff9c,%edi    ; RDI = -100 (AT_FDCWD)
    mov    $0x101,%eax         ; RAX = 257 (openat syscall)
    syscall

OBSERVE:
    Your call: open(file, flags)
    Kernel call: openat(AT_FDCWD, file, flags, mode)

MATH (AT_FDCWD = -100):
    -100 in two's complement (32-bit):
    100 decimal = 0x64
    Invert: 0xffffff9b
    Add 1:  0xffffff9c

DO:   Verify AT_FDCWD.
RUN:  grep AT_FDCWD /usr/include/linux/fcntl.h

OUTPUT:
    #define AT_FDCWD -100

DO:   Verify syscall number.
RUN:  grep 257 /usr/include/asm/unistd_64.h

OUTPUT:
    #define __NR_openat 257

INQUIRY:
    Q: WHY does libc call openat() instead of open()?
    A: The kernel deprecated the old sys_open syscall.
       openat() is the unified syscall (more general).
       AT_FDCWD (-100) means "use current working directory".
       This makes openat(-100, file, flags) == old open(file, flags).

    Q: What is the benefit?
    A: openat() can also open files relative to ANY directory (not just cwd).
       Example: openat(dirfd, "file", O_RDONLY) opens relative to dirfd.
       This is used for security (avoid TOCTOU races).

AXIOM: libc::open(file,flags) calls kernel::openat(-100, file, flags, 0).


================================================================================
STEP 19: FAILURE PREDICTIONS
================================================================================

Now you understand the chain. Predict what breaks.

FAILURE 1: File does not exist
    PREDICTION: open() returns -1, errno = 2 (ENOENT)
    VERIFY: strace ./minimal_open 2>&1 | grep openat

FAILURE 2: No permission
    PREDICTION: open() returns -1, errno = 13 (EACCES)
    VERIFY: chmod 000 somefile; strace ./minimal_open

FAILURE 3: Too many open files
    PREDICTION: open() returns -1, errno = 24 (EMFILE)
    VERIFY: ulimit -n 10; run program opening 20 files

MATH (errno values):
    RUN: grep -E "ENOENT|EACCES|EMFILE" /usr/include/asm-generic/errno-base.h
    OUTPUT:
        #define ENOENT  2
        #define EACCES 13
        #define EMFILE 24


================================================================================
STEP 20: THE COMPLETE CHAIN
================================================================================

SOURCE:     open("somefile", O_RDWR)
                |
PREPROCESS: open("somefile", 02)
                |
COMPILE:    call 0x00000000 (zeros)
                |
ASSEMBLE:   reloc entry: offset=0x21, symbol=open, addend=-4
                |
LINK:       call 0x1050 (open@plt)
            PLT at 0x1050: jmp *0x3fd0
            GOT at 0x3fd0: 0x1030 (stub)
                |
RUNTIME:    First call:
                PLT -> GOT -> stub -> resolver -> libc
                Resolver patches GOT: 0x3fd0 = 0x7ffff7d1b150
                |
            Subsequent calls:
                PLT -> GOT -> libc (direct!)
                |
LIBC:       Transform: open(file, flags) -> openat(-100, file, flags, 0)
            Register setup: RDI=-100, RSI=file, RDX=flags, RAX=257
            syscall
                |
KERNEL:     sys_openat() executes
            Returns file descriptor or error

================================================================================
END OF WORKSHEET
================================================================================
Every step derived from previous step.
Every number verified by command.
No magic. No new things without derivation.