Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mastodon

Introduction

This is my personal collection of notes, documentation and miscellaneous wiki articles.

Some of it is notes to self, some instructions of how to do things and some are simply information that I’ve found interesting.

Here are main sections so far:

Use this in whichever way you want, this is not my property!

[Valid RSS]

Wiki

This is a collection of wiki articles on different topics from how to configure DisplayLink on Debian 13 to Windows command line cheatsheet.

mdbook Setup Guide

Tutorial: Reading objdump Output, Line by Line

We’ll write a tiny C program, compile it, and disassemble it — then go through the actual x86-64 assembly the compiler produced, instruction by instruction.

The program

#include <stdio.h>
#include <limits.h>

void print_binary(unsigned int x)
{
    for (int i = sizeof(x) * CHAR_BIT - 1; i >= 0; i--)
        putchar((x >> i) & 1 ? '1' : '0');
    putchar('\n');
}

int main(void)
{
    int x = 45;
    printf("%d\n", x);
    print_binary((unsigned int) x);
    return 0;
}

Save this as example.c.

Compiling with debug info, no optimization

gcc -g -O0 -o example example.c

Two flags matter a lot here:

  • -g embeds debug information (source file names, line numbers, variable info) into the binary — this is what lets objdump -S interleave your original C source alongside the assembly.
  • -O0 disables optimization. This is deliberate for learning: an optimized build (-O2) would reorder, merge, and eliminate instructions aggressively, making the assembly much harder to map back to your source line-by-line. -O0 gives you a close-to-literal, unoptimized translation — useful for understanding, not for shipping.

Disassembling

objdump -dS --disassemble=main example
objdump -dS --disassemble=print_binary example
  • -d disassembles executable sections.
  • -S interleaves original source lines (requires -g at compile time).
  • --disassemble=<function> restricts output to just that one function, instead of dumping the entire binary.

Part 1: main, line by line

00000000000011be <main>:

The label. main starts at address 0x11be within this binary.

Function prologue

int main(void)
{
    11be:	f3 0f 1e fa          	endbr64
    11c2:	55                   	push   %rbp
    11c3:	48 89 e5             	mov    %rsp,%rbp
    11c6:	48 83 ec 10          	sub    $0x10,%rsp

This block is boilerplate that appears at the start of nearly every function compiled with -O0 — the function prologue. It sets up this function’s own private workspace (its stack frame).

  • endbr64 — a CPU security feature (Intel CET / “Control-flow Enforcement Technology”), marking this as a valid landing point for indirect jumps/calls. Not something you write yourself; the compiler inserts it automatically as a defense against certain exploit techniques. Safe to mentally skip past for understanding program logic.
  • push %rbp — saves the caller’s base pointer (%rbp) onto the stack, so it can be restored when this function returns. %rbp conventionally points at the base of the current function’s stack frame — this instruction preserves whoever was using it before us.
  • mov %rsp,%rbp — copies the current stack pointer (%rsp) into %rbp, establishing this function’s own frame base. From here on, local variables are addressed as offsets from %rbp.
  • sub $0x10,%rsp — moves the stack pointer down by 16 bytes, reserving 16 bytes of scratch space on the stack for this function’s local variables (here, just the one int x, though the compiler reserved more than the strict minimum — normal at -O0, and often related to stack alignment requirements).

This is the concrete, physical version of the abstract “stack frame” / “activation record” concept from earlier in this series of questions — you’re looking at the actual instructions that create one.

int x = 45;

    int x = 45;
    11ca:	c7 45 fc 2d 00 00 00 	movl   $0x2d,-0x4(%rbp)
  • -0x4(%rbp) is the memory location the compiler chose for the local variable x — 4 bytes below the frame base. This is x’s actual address in memory for the lifetime of this function call — the same kind of address you’ve been taking with &x at the source level.
  • $0x2d is 45 in hex (0x2d = 45). movl (“move long word”) writes this 4-byte immediate value directly into that memory location — this one instruction is x = 45.

printf("%d\n", x);

    printf("%d\n", x);
    11d1:	8b 45 fc             	mov    -0x4(%rbp),%eax
    11d4:	89 c6                	mov    %eax,%esi
    11d6:	48 8d 05 27 0e 00 00 	lea    0xe27(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    11dd:	48 89 c7             	mov    %rax,%rdi
    11e0:	b8 00 00 00 00       	mov    $0x0,%eax
    11e5:	e8 86 fe ff ff       	call   1070 <printf@plt>

This block is entirely about the x86-64 calling convention — the rule for how arguments get passed to a function. On Linux x86-64, the first several integer/pointer arguments go in specific registers, in order: %rdi, %rsi, %rdx, %rcx, %r8, %r9.

  • mov -0x4(%rbp),%eax — loads x’s value (45) from memory back into the %eax register. (The compiler stored it to memory a moment ago and now has to reload it — this kind of redundant memory round-trip is exactly the sort of thing -O2 optimization would eliminate; at -O0 the compiler doesn’t bother.)
  • mov %eax,%esi — moves x’s value into %esi, which is the register for the second argument to printf. (printf’s arguments are the format string, then x — so x is argument #2.)
  • lea 0xe27(%rip),%rax — lea = “load effective address.” This computes the address of the string literal "%d\n" (stored elsewhere in the binary, referenced here relative to the current instruction pointer, %rip) and puts that address into %rax. The comment # 2004 <_IO_stdin_used+0x4> is objdump helpfully telling you the resolved absolute address and what section it falls in.
  • mov %rax,%rdi — moves that string address into %rdi, the register for the first argument (the format string).
  • mov $0x0,%eax — sets %eax to 0. This looks unrelated to your code, but it’s part of the x86-64 calling convention specifically for variadic functions like printf: %eax must hold the number of vector/floating-point registers used for the call (0 here, since we’re passing plain integers, no floats).
  • call 1070 <printf@plt> — the actual call. <printf@plt> means this jumps through the PLT (Procedure Linkage Table) — a layer of indirection used because printf lives in the dynamically-linked C library (libc), not in this binary itself; the PLT resolves the real address of printf at load time (or on first call).
    print_binary((unsigned int) x);
    11ea:	8b 45 fc             	mov    -0x4(%rbp),%eax
    11ed:	89 c7                	mov    %eax,%edi
    11ef:	e8 75 ff ff ff       	call   1169 <print_binary>
  • Reload x from memory into %eax.
  • Move it into %edi — the register for the first argument. Note: the (unsigned int) cast in your source doesn’t generate any actual instruction here — reinterpreting a 4-byte int as a 4-byte unsigned int involves no bit-pattern change at all, only a change in how later instructions interpret the same bits (recall the earlier discussion on reinterpreting bytes via pointer casts — this is the same idea: the cast is purely a compile-time typing decision, with zero runtime cost here).
  • call 1169 <print_binary> — this time, no @plt — print_binary is defined right here in this same binary, so the call target is a direct, known address (0x1169), no dynamic-linking indirection needed.

return 0; and epilogue

    return 0;
    11f4:	b8 00 00 00 00       	mov    $0x0,%eax
}
    11f9:	c9                   	leave
    11fa:	c3                   	ret
  • mov $0x0,%eax — the x86-64 convention for returning an integer value is: put it in %eax. This is exactly how main’s return 0 becomes the process’s exit status.
  • leave — a shorthand instruction that undoes the prologue in one step: restores %rsp from %rbp (deallocating the stack frame) and pops the saved %rbp back (undoing the very first push %rbp). This is the function epilogue.
  • ret — pops the return address (pushed automatically by the call instruction that got us here) and jumps back to the caller.

Part 2: print_binary, line by line

This is the more interesting one — it’s a real loop with a real branch, so you get to see how C control flow actually becomes machine code.

0000000000001169 <print_binary>:
void print_binary(unsigned int x)
{
    1169:	f3 0f 1e fa          	endbr64
    116d:	55                   	push   %rbp
    116e:	48 89 e5             	mov    %rsp,%rbp
    1171:	48 83 ec 20          	sub    $0x20,%rsp
    1175:	89 7d ec             	mov    %edi,-0x14(%rbp)

Same prologue pattern as before, plus one new instruction:

  • mov %edi,-0x14(%rbp) — x arrived in %edi per the calling convention (it’s the argument register). This instruction immediately copies it out into x’s own stack slot, at -0x14(%rbp). This is standard -O0 behavior: even a parameter that arrived in a register gets spilled to memory right away, purely so the rest of the function can treat it uniformly like any other local variable.

The for loop setup

    for (int i = sizeof(x) * CHAR_BIT - 1; i >= 0; i--)
    1178:	c7 45 fc 1f 00 00 00 	movl   $0x1f,-0x4(%rbp)
    117f:	eb 2a                	jmp    11ab <print_binary+0x42>
  • movl $0x1f,-0x4(%rbp) — this is i = sizeof(x) * CHAR_BIT - 1, fully computed at compile time. 0x1f = 31 decimal. The compiler already knows sizeof(unsigned int) is 4 and CHAR_BIT is 8, so 4 * 8 - 1 = 31 — no runtime multiplication ever happens; the compiler folded the whole expression into a single constant. i lives at -0x4(%rbp), a separate stack slot from x’s -0x14(%rbp).
  • jmp 11ab — jumps straight down to the loop’s condition check (address 0x11ab, shown further below), skipping the loop body on this first pass through. This is a very characteristic pattern for compiled for loops: check the condition before the first iteration by jumping to the check first, then looping back up to the body only if the check passes. It restructures your for(init; cond; incr) body into something closer to “init; goto check; body: …; incr; check: if (cond) goto body;” — logically identical to your source, just rearranged so the condition test is a single piece of code reused both for the “should I run at all” question and the “should I loop again” question.

The loop body: putchar((x >> i) & 1 ? '1' : '0');

        putchar((x >> i) & 1 ? '1' : '0');
    1181:	8b 45 fc             	mov    -0x4(%rbp),%eax
    1184:	8b 55 ec             	mov    -0x14(%rbp),%edx
    1187:	89 c1                	mov    %eax,%ecx
    1189:	d3 ea                	shr    %cl,%edx
    118b:	89 d0                	mov    %edx,%eax
    118d:	83 e0 01             	and    $0x1,%eax
    1190:	85 c0                	test   %eax,%eax
    1192:	74 07                	je     119b <print_binary+0x32>
    1194:	b8 31 00 00 00       	mov    $0x31,%eax
    1199:	eb 05                	jmp    11a0 <print_binary+0x37>
    119b:	b8 30 00 00 00       	mov    $0x30,%eax
    11a0:	89 c7                	mov    %eax,%edi
    11a2:	e8 b9 fe ff ff       	call   1060 <putchar@plt>

This is the whole expression (x >> i) & 1 ? '1' : '0', broken into its real, individual pieces — a great example of how much machinery one line of C can expand into:

  • mov -0x4(%rbp),%eax — load i into %eax.
  • mov -0x14(%rbp),%edx — load x into %edx.
  • mov %eax,%ecx — copy i into %ecx. This looks redundant, but it isn’t: x86’s shift instructions have a hardware quirk where the shift amount, when it’s a variable (not a compile-time constant), must be supplied specifically in the %cl register (the low byte of %ecx) — there’s no shift-by-any-register form. The compiler is forced to shuffle i into %ecx purely to satisfy this instruction encoding rule.
  • shr %cl,%edx — the actual shift! shr = shift right, logical (zero-fill) — matches unsigned int x, confirming exactly the logical-vs-arithmetic distinction from your earlier question: because x is unsigned, the compiler emits shr, not sar. This computes x >> i, in place, in %edx.
  • mov %edx,%eax — copy the shift result into %eax.
  • and $0x1,%eax — the & 1 — masks off every bit except the lowest one. This is the literal machine-level version of the bitmask extraction technique from earlier in this conversation.
  • test %eax,%eax — a common idiom for “is this value zero?” — it ANDs %eax with itself (without storing the result anywhere) purely to set the CPU’s internal flags based on the value.
  • je 119b — “jump if equal” (i.e., jump if the last test found zero) — jump down to the '0' branch. This is the compiled form of your ternary’s false path.
  • mov $0x31,%eax / jmp 11a0 — 0x31 is ASCII '1' (decimal 49). This is the ternary’s true branch: load the character '1', then jump past the other branch.
  • mov $0x30,%eax (at label 119b) — 0x30 is ASCII '0' (decimal 48) — the false branch’s value.
  • mov %eax,%edi / call 1060 <putchar@plt> — same calling convention pattern as before: put the argument (whichever character was selected) into %edi, call putchar through the PLT (dynamically linked, like printf).

Loop increment and condition check

    for (int i = sizeof(x) * CHAR_BIT - 1; i >= 0; i--)
    11a7:	83 6d fc 01          	subl   $0x1,-0x4(%rbp)
    11ab:	83 7d fc 00          	cmpl   $0x0,-0x4(%rbp)
    11af:	79 d0                	jns    1181 <print_binary+0x18>
  • subl $0x1,-0x4(%rbp) — i--, done directly in memory (subtract 1 from i’s stack slot).
  • cmpl $0x0,-0x4(%rbp) — compares i against 0 (sets flags based on i - 0, without storing the subtraction result anywhere — same “compute flags only” idea as test above).
  • jns 1181 — “jump if not sign” — i.e., jump back up to the loop body (address 0x1181) if the comparison result was non-negative, meaning i >= 0 still holds. This single instruction is the entire compiled form of your i >= 0 loop condition. Once i becomes -1, this jump is no longer taken, and execution falls through to what’s next.

putchar('\n'); and epilogue

    putchar('\n');
    11b1:	bf 0a 00 00 00       	mov    $0xa,%edi
    11b6:	e8 a5 fe ff ff       	call   1060 <putchar@plt>
}
    11bb:	90                   	nop
    11bc:	c9                   	leave
    11bd:	c3                   	ret
  • mov $0xa,%edi — 0x0a is ASCII newline (\n). Loaded directly as a constant — no memory lookup needed, since '\n' is a literal known at compile time.
  • call 1060 <putchar@plt> — same putchar call pattern as inside the loop.
  • nop — “no operation” — does literally nothing. Compilers sometimes insert these for instruction alignment purposes (certain addresses are faster for the CPU to fetch/decode from); not meaningful to your program’s logic.
  • leave / ret — same epilogue pattern as main.

Things worth noticing across both functions

  • Every local variable got its own stack slot, addressed as a negative offset from %rbp — this is the concrete reality behind the abstract idea of “a variable’s address,” which you’ve been reasoning about symbolically (&x) all through this conversation. Here you can see the actual offset the compiler picked.
  • Constant expressions get folded at compile time. sizeof(x) * CHAR_BIT - 1 never executes as a multiplication and subtraction at runtime — it’s baked into a single movl $0x1f, ... instruction. The compiler evaluates anything it can determine ahead of time, once, rather than making the CPU redo it on every run.
  • Function calls follow a strict, mechanical calling convention — arguments in %rdi, %rsi, %rdx, … in order, return value in %eax/%rax. This is exactly what makes it possible for main (in this binary) to call printf (compiled separately, in glibc, decades ago) and have it just work — both sides agree on this convention without ever having seen each other’s source code.
  • shr (not sar) appears because x is declared unsigned — direct, physical confirmation of the logical-vs-arithmetic shift discussion: change unsigned int x to int x in the function signature and recompile, and you’d see sar emitted instead for the exact same source-level >> operator.
  • -O0 is verbose and redundant on purpose — notice how often a value gets stored to memory and immediately reloaded (e.g. x is loaded into %eax, stored, then reloaded again just a few instructions later). A real optimizing build (-O2) would keep such values in registers throughout and eliminate most of this — but that would also make the assembly much harder to map back to individual source lines, which is exactly why -O0 is the right choice for a tutorial like this one, and -O2 is what you’d actually ship.

Try it yourself

# See the difference optimization makes:
gcc -g -O2 -o example_opt example.c
objdump -dS --disassemble=print_binary example_opt

Compare this against the -O0 version above — at -O2, the compiler will likely eliminate most of the memory round-trips, possibly unroll part of the loop, and generally produce something much less line-by-line traceable back to source — a good next step once this version makes sense.

Bitwise Operators Cheatsheet (C)

The operators, one bit at a time

OpNameRule (per bit pair a,b)
&AND1 only if both are 1
|OR1 if either is 1
^XOR1 if they differ
~NOTflips the single bit
<<left shiftslides bits toward higher value (×2 per shift)
>>right shiftslides bits toward lower value (÷2 per shift)

What each operator does when combined with a mask

This is the part worth memorizing — what a 1-bit vs a 0-bit in your mask means, per operator:

Opmask bit = 1 means…mask bit = 0 means…
x & maskkeep that bit of x unchangedforce to 0
x | maskforce to 1keep that bit of x unchanged
x ^ maskflip that bit of xkeep that bit of x unchanged
x & ~maskforce to 0keep that bit of x unchanged

Everything below is a consequence of just this table.


Common idioms

x & mask        // TEST/EXTRACT — isolate specific bits, zero the rest
x | mask        // SET — force specific bits to 1, leave rest alone
x & ~mask       // CLEAR — force specific bits to 0, leave rest alone
x ^ mask        // TOGGLE — flip specific bits, leave rest alone
~x              // flip EVERY bit
~x ^ mask       // flip everything, then flip mask's 1-bits back
                //   → net effect: bits under mask=1 stay original,
                //     bits under mask=0 end up flipped
x ^ ~mask       // identical result to ~x ^ mask (see Identities below)
(x >> i) & 1    // test whether bit i specifically is set (0 or 1)
x & (x - 1)     // clear the lowest set bit
x & -x          // isolate the lowest set bit (two's complement trick)
x | (1 << i)    // set bit i
x & ~(1 << i)   // clear bit i
x ^ (1 << i)    // toggle bit i

Masks by width — remember the implicit zero-padding

A literal like 0xFF is widened to match the operand’s width by padding with zeros on the left before the operation runs:

0xFF  on a 32-bit int  →  0x000000FF

That’s why x & 0xFF keeps only the last byte: the mask is 1-bits for the byte you want, and implicit 0-bits for everything above it.

Want to isolateMask
last byte0xFF
last 2 bytes0xFFFF
last nibble (4 bits)0xF
bit i only1 << i
everything except the last byte~0xFF

Shifts

x << k     // multiply by 2^k. Fills k zeros on the right. Drops top k bits.
x >> k     // divide by 2^k (roughly).
  • Right shift on unsigned: always logical — fills with 0s on the left. Well-defined, no ambiguity.
  • Right shift on signed, negative x: implementation-defined — in practice almost universally arithmetic (sign-extends, fills with 1s) on real hardware/compilers, but not guaranteed by the standard.
  • Undefined behavior: shifting by a negative amount, or by an amount >= the width of the type (x << 32 on a 32-bit int). Always guard:
    if (k >= 0 && k < (int)(sizeof(x) * CHAR_BIT)) y = x << k;
    

Useful identities

~x ^ x        == all 1s   (a bit and its complement always differ)
~a ^ b        == a ^ ~b   (complementing either single operand of XOR
                            gives the same result)
a ^ b ^ b     == a         (XOR with the same value twice cancels —
                            this is the basis of the XOR swap)
a & a         == a
a | a         == a
a & ~a        == 0
a | ~a        == all 1s
~(~x)         == x
(x & ~y) | (~x & y) == x ^ y

Set-operation view (bit vectors as sets)

If a bit vector encodes a set (bit i set ⟺ element i is in the set):

a & b     // intersection
a | b     // union
a ^ b     // symmetric difference (elements in exactly one of the two)
~a        // complement (relative to the full universe of bits)
a & ~b    // set difference (A minus B)

Gotchas

  • Operator precedence: &, |, ^ bind looser than ==/!= and comparisons. if (x & mask == 1) parses as x & (mask == 1) — almost always a bug. Always parenthesize: if ((x & mask) == 1).
  • &&/|| vs &/|: the double-character versions are logical operators (short-circuiting, operate on “truthy/falsy” as a whole, return 0/1) — completely different from the single-character bitwise versions. Easy to typo one for the other.
  • Signed shift of negative numbers: right-shifting a negative signed int is implementation-defined, not guaranteed zero-fill. Cast to unsigned first if you need guaranteed logical-shift behavior.
  • Mask width mismatches: x & 0xFF on a 64-bit x still only keeps the last byte — the mask pads with zeros to match, which is usually what you want, but worth double-checking when mixing types of different widths in one expression.

Quick self-test

Given x = 0x87654321 (32-bit):

ExpressionResultWhy
x & 0xFF0x00000021keep last byte, zero rest
x | 0xFF0x876543FFforce last byte to all 1s
x ^ 0xFF0x876543DEflip only the last byte
~x0x789ABCDEflip everything
~x ^ 0xFF0x789ABC21flip everything, then un-flip last byte
x << 40x76543210shift left 4, drop top nibble, zero-fill right
x >> 4 (unsigned)0x08765432shift right 4, zero-fill left

Asymmetric GPG Encryption

GPG encryption

1. Check if you already have a GPG key pair

gpg --list-secret-keys --keyid-format=long

2. If not, generate one

gpg --full-generate-key

Pick RSA and RSA (default), 4096 bits, set an expiration if you want, and use your real email — it’s the identifier GPG uses to find your key.

3. Encrypt the file to your own key (so only you can decrypt it)

gpg --encrypt --recipient your@email.com secret.txt

This produces secret.txt.gpg (binary). If you want an ASCII-safe version to paste into git/email, add --armor:

gpg --encrypt --armor --recipient your@email.com secret.txt

This produces secret.txt.asc.

4. Decrypt / view it

gpg --decrypt secret.txt.gpg

GPG will prompt for your private key’s passphrase, then print the plaintext to stdout. Redirect if you want a file back:

gpg --decrypt secret.txt.gpg > secret.txt

5. Once you trust the encrypted copy, shred the original

shred -u secret.txt

Otherwise the plaintext is still sitting on disk regardless of how locked the .gpg copy is.

If you want a different person to be the only one who can decode it

Get their public key (gpg --import theirkey.asc or fetch from a keyserver), then encrypt with --recipient their@email.com instead of your own. Only their private key can open it.

Extracting ufw Logs from VPS for Analysis

The goal is to extract ufw firewall logs from UpCloud server to my local machine and convert it to a DuckDB database.

Copying Logs on the Server

My admin user has no root privileges on the server, so I first need to copy all ufw logs from /var/log/ to a /tmp/ufw-logs folder.

mkdir /tmp/ufw-logs
sudo cp /var/log/ufw* /tmp/ufw-logs
sudo chown -R $USER:$USER /tmp/ufw-logs

Downloading Logs to Local Machine

Now get those logs from the server:

rsync -avz UpCloud:'/tmp/ufw-logs/ufw*' ~/upcloud_ufw_logs/

Parsing Logs and Saving to DuckDB Database

Now parse those logs and convert them into a .duckdb file.

pyenv activate analytics
python ~/Coding/python-coding/ingest_ufw.py ~/upcloud_ufw_logs ~/ufw.duckdb

Start Exploring the Logs

Launch DuckDB and start poking around.

duckdb ~/ufw.duckdb
D: ufw: CALL start_ui();

Documentation

More extensive documentation, tutorials, guides in the form of articles.

Content

Forgejo Self-Hosting

Forgejo is a self-hosted git service. Think of it as GitHub or Codeberg, a platform on which you can store your git repositories, make commits, pushes, clones etc. Forgejo provides you with ability to host your own git repositories on a server.

I wanted to do it for some time although I don’t have many repositories. I have my server setup on UpCloud which is an EU cloud service provider based in Helsinki. They have their data centers all over Europe, nice web interface and reasonable pricing. My own setup is a 2 cores CPU, 8GB RAM and 40GB disk storage running Debian 13. It is pretty light-weight. I have firewall and nginx configured there and I host this Knowledge Base there as well as my website.

Let’s now setup our Forgejo instance on the server and migrate a couple of repositories there.

Forgejo Setup

Prepare a Subdomain

I use Porkbun for my domain management. To create a new subdomain, go to your domain DNS records and add an A record with git as subdomain host and pointing to the IP of your VPS.

After that you need to get a certificate for your new subdomain so that you can have an HTTPS connection. Log in to your server via SSH, then do:

sudo certbot --nginx -d git.yourdomain.com

This will run the process of getting you a new certificate.

Installing Forgejo

You are now ready to install Forgejo. There are a few options available. I don’t want to mess with Docker so I will install Forgejo binary on my VPS. Lookup the latest release binary on Frogejo git repository. Then in your VPS do the following:

wget -O forgejo https://codeberg.org/forgejo/forgejo/releases/download/<version-number>/forgejo-<version>-<architecture>
chmod +x forgejo
sudo mv forgejo /usr/local/bin/forgejo

wget -O will download the binary and save it under name forgejo. Then you make it executable and move it to where you naturally store system-wide binaries on Linux.

Creating Dedicated User

As always it is a good practice to create a separate user to manage services such as git. I already have git user on my VPS because I am managing bare git repositories with it. Instead of using that user, I will create a dedicated forgejo user.

sudo adduser --system --group --disabled-password --home /home/forgejo forgejo
sudo mkdir -p /var/lib/forgejo /etc/forgejo
sudo chown forgejo:forgejo /var/lib/forgejo

/var/lib/forgejo directory is where our git repositories, logs and all other forgejo stuff will live. /etc/forgejo will contain app.ini configuration file. It is critical to make sure that forgejo user owns those directories!

Setting Up systemd Service

To make sure that forgejo is always running on our VPS, we need to setup a systemd service for it. Let’s create /etc/systemd/system/forgejo.service which will contain service configuration:

[Unit]
Description=Forgejo
After=network.target

[Service]
User=forgejo
Group=forgejo
WorkingDirectory=/var/lib/forgejo
ExecStart=/usr/local/bin/forgejo web --config /etc/forgejo/app.ini
Restart=always
Environment=USER=forgejo HOME=/home/forgejo FORGEJO_WORK_DIR=/var/lib/forgejo

[Install]
WantedBy=multi-user.target

Side Note: This is the final version services file but be prepared to troubleshoot if forgejo web interface will not be able to find certain configuration directories. The variable FORGEJO_WORK_DIR=/var/lib/forgejo is what I eventually needed to add for everything to work. Generally, though to troubleshoot any issues use this as a guidance:

  • check folders permissions for key forgejo errors
  • check service logs with journalctl -u forgejo -n 50 --no-pager
  • check /etc/forgejo/app.ini for any missing configuration

Create nginx Reverse Proxy

Now it is time to setup nginx to direct requests to our git.yourdomain.com server via localhost on port 3000 that is the port that forgejo uses by default. Create a file /etc/nginx/sites-available/forgejo with the following content:

server {
    server_name git.yourdomain.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Now reload nginx to pick up the newly added site:

sudo ln -s /etc/nginx/sites-available/forgejo /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

This should return no errors. Your forgejo instance should now be ready to go however…

SSH

I manage my VPS with SSH remote access. Naturally, port 22 is occupied for this. By default forgejo also wants to bind to port 22 which will conflict with how I work now. In order to avoid that we will make forgejo use port 2222 instead. But before we do that we need to take care of firewall.

Firewall Config

The way it works on UpCloud, is that they have their own network firwall which you can configure + you can configure your own rules on your VPS. My rules are simple, allow incoming traffic on ports 22, 80, 443, basic stuff. Now we need to add port 2222 to that so that our forgejo instance could accept requests.

sudo ufw status verbose # check which rules you already have
# Then do
sudo ufw allow 2222/tcp # allow incoming traffic on port 2222
sudo ufw status verbose # confirm that rule is set

In UpCloud network firewall I have configured the firewall the same way.

At this point you should be ready to run forgejo setup wizard.

Forgejo Setup Wizard

First, make sure that your forgejo service is running. So do this:

sudo systemctl enable forgejo --now
sudo systemctl start forgejo
sudo systemctl status forgejo

This should return a green active status. Now open git.yourdomain.com in your browser which should open a Forgejo configuration page. It should pick up all the directories correctly from your systemd configuration. If it does not, then you will need to find the reason for that (see above for troubleshooting guidance). Hopefully, all configuration directories are picked up. All you need to do now is to:

  • select database that you want to use. I use sqlite as it is more than enough for me but mysql and postgres are alsow available.
  • change SSH port from 22 to 2222.
  • below on the page expand Administration section and configure your admin account. This will allow you to login to your forgejo.

After all that just click Install Forgejo. This will take you to final Forgejo interface which looks a lot like Codeberg. You will be singed in already so now you can work on creating new repositories and all that good stuff.

Testing Forgejo Instance

To test that everything is working first just hit forgejo with this:

ssh -p 2222 forgejo@git.yourdomain.com

You want get a shell but something like Permission Denied should appear which means that we can reach out to our forgejo instance. You can also create a test repository on git.yourdomain.com and you should be able to clone it via HTTPS. We need to configure our SSH client so that it we can also do everything via ssh.

ssh-keygen -t ed25519 -a 100 # generates a new key

I try to use different keys for different services so I rename the default key file name to id_ed25519_forgejo. No that key is generated we need to copy it and put into forgejo.

xclip -selection clipboard < ~/.ssh/id_ed25519_forgejo.pub

Now go to git.yourdomain.com -> Settings -> SSH and GPG and click on Add key in SSH section. Paste your copied key into the text field and save. Then also click on Verify and follow the instructions that will appear on the screen. After this you should be ready to setup your local SSH client. Forgejo has a good guide on how to configure your SSH keys. Now on your local machine you need to explain your SSH client what is forgejo and how to connect to it. Since I use a custom name for a key file I will add the following to my ~/.ssh/config:

Host git.nikolaydudaev.com
    HostName git.yourdomain.com
    Port 2222
    User forgejo
    IdentityFile ~/.ssh/id_ed25519_forgejo

Now you should be ready to do all the cloning and pushing that you want via ssh. Simple flow is:

  • create new repository on git.yourdomain.com. It will show you instructions how to connect to your local repository.
  • generally, even if you have other remotes configured you can always add another one with git remote add forgejo <repo URL>

And voila! You are now hosting your own git repositories on the VPS that you are managing yourself! Congratulations!

Forgejo Update

Forgejo need to be periodically updated to fix any security issues, bugs etc. This is how to do it.

Backup

Make a dump of current data you have. SSH to your server and do this:

forgejo dump

This creates a zip file with your data in the directory you ran the command.

Stop the Forgejo Service

sudo systemctl stop forgejo

Install the Update

Find the newest release binary and downloaded (like the time you were installing it):

wget -O forgejo https://codeberg.org/forgejo/forgejo/releases/download/<version-number>/forgejo-<version>-<architecture>
chmod +x forgejo
sudo mv forgejo /usr/local/bin/forgejo

Start the Forgejo Service

Now start the service again and immediately check the logs for any errors:

sudo systemctl start forgejo
sudo journalctl -u forgejo -n 30 --no-pager

Make a Test Repo

To make sure everything works, make a test repo and try to clone it to your local machine via ssh and push changes back. If everything works as it should, you have an updated version of forgejo!

Conclusion

There were some hickups along the way while I was doing this. They were related to the configuration directories that Forgejo installation wizard could not find or did not have proper permissions for. But eventually, after some troubleshooting, everything worked smooth and nice. I am very excited about this now as I think more and more about Digital Autonomy these days!

Reverse Engineering

This section contains my notes on learning Reverse Engineering and things related to that.

Sections:

Introduction

A few important terms and definitions:

  • ELF - Executable and Linking Format

Tools useful for this work:

  • objdump
  • strace
  • ltrace
  • ftrace
  • objcopy
  • readelf

Useful devices and files:

  • /proc/<pid>/maps
  • /proc/kcore
  • /boot/System.map
  • /proc/kallsyms
  • /proc/iomem
  • LD_PRELOAD
  • LD_SHOW_AUXV

Devices and Files on Linux

Here is a list of useful devices and files on Linux:

ELF

Linker-related Environment Points

LD_PRELOAD

This env variable can be set to specify a library path that should be dynamically linked before any other libraries. This has the effect of allowing functions and symbols from the preloaded library to override the ones from the other libraries that are linked afterwards.

LD_SHOW_AUXV

This env variable tells the program loader to display the program’s auxiliary vector during runtime. The auxiliary vector is information that is placed on the program’s stack (by the kernel’s ELF loading routine), with information that is passed to the dynamic linker with certain information about the program.

Linker

Linker on Linux has the linker script that actually shapes a program’s layout with regard to sections, memory, and symbols. Default linker script can be viewed with ld --verbose command.

The ld linker has a complete language that it interprets when it is taking input files, and it uses the language to determine how the output file will be organized.

A linker script can be specified with gcc using -T flag.

Linux Tools for Binary Analysis

Programming

C

Notes on Pointers and Arrays

Pointer Arithmetic

Adding an Integer to a Pointer

Adding an integer j to a pointer p yields a pointer to the element j places after one that p points to. More precisely, if p points to the array element a[i], then p + j points to a a[i + j] (provided, of course, that a[i + j] exists).

Subtracting and Integer from a Pointer

If p points to the array element a[i], then p - j points to a[i - j].

Subtracting One Pointer from Another

When one pointer is subtracted from another, the result is the distance (measured in array elements) between the pointers. thus if p points to a[i] and q points to a[j], then p - q equals to i - j.

Compound Literals

Example: int *p = (int []){1,2,3};. (int []){1,2,3} is an unnamed compound literal. IMPORTANT: in this scenario p has no meta information about the array compound literal because it only points to a first element of an array. This is called array-to-pointer decay. You can still access elements of an array or use pointer arithmetic, however, you have no idea about what the length or an array is and where it ends.

* and ++

We can process arrays with combination of * and ++/-- operators. Here are a some common idioms:

ExpressionMeaning
*p++ or *(p++)First, get the value of *p then increment p
(*p)++First, get the value of *p then increment *p
*++p or *(++p)First, increment p, then get the value of *p after increment
++*p or ++(*p)First, increment *p, then get value of *p after increment

Array Name as Pointer

In int a[] = {1,2,3} name a can be used as a pointer to first element of an array.

Array Arguments

Array name passed to a function is always treated as a pointer. This means that:

  • array is not copied (like an ordinary variable) and is not protected from change
  • to protect array from change include const to array parameter
  • there is no penalty for passing large array to a function as it is not copied
  • for clarity, array parameter may be declared as pointer, e.g. int *arr

Same as array name can be used as a pointer to first element of an array, pointer to an array can be used as an array name and can be subscripted accordingly.

Strings

Continued String Literal

When two or more string literals are adjacent (separated only by whitespace), the compiler will join them into a single string. Examples:

printf("All "
       "of "
       "this will be "
       "printed on one line\n");

Format specifiers can be used that way too.

How String Literals Are Stored

String literal is an array of characters. When compiler finds a string literal of length n, it reserves n + 1 bytes of memory for the string. Null character (byte with all bits set to 0) - \0 - will be added to it to mark the end of string. Since it is stored as an array, compiler treats it as a pointer of type char *.

When declaring an array of characters always make the array one character longer to accomodate for null character which is expected by compiler. If character array has less elements than declared, the length of the string depends on the position of the null character not on the length of the array.

Modifying String Literal

Attempting to modify a string literal causes undefined behavior.

Initializing a String Variable

char str[6] = "Hello";

Compiler will adds null character at the end. "Hello" in this case is not a string literal, C treats it as an array of characters.

If initializer is too short to fill in the array, e.g.

char str[10] = "Hello";

Compiler will fill in the rest of the array with 0's that is with null character.

If initializer is exactly of the same length as the variable, compiler will allow it and no null character will be added. In this case the array will be unusable as a string though.

Finally, length can be omitted, then compiler will compute the length of the array. This does not mean that the length of the array can be changed later!

Character Arrays vs Character Pointers

TypeDeclarationSpecifics
Character Arraychar str[]- Can pass it to a function that expects either pointer or an array
- Elements of array can be modified
- str is the name of an array
Character Pointerchar *str = "Some String"- Can pass it to a function that expects either pointer or an array
- Points to string literal which cannot be modified
- Can point to other strings during execution