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!
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:
-gembeds debug information (source file names, line numbers, variable info) into the binary — this is what letsobjdump -Sinterleave your original C source alongside the assembly.-O0disables 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.-O0gives 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
-ddisassembles executable sections.-Sinterleaves original source lines (requires-gat 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.%rbpconventionally 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 oneint 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 variablex— 4 bytes below the frame base. This isx’s actual address in memory for the lifetime of this function call — the same kind of address you’ve been taking with&xat the source level.$0x2dis45in hex (0x2d = 45).movl(“move long word”) writes this 4-byte immediate value directly into that memory location — this one instruction isx = 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— loadsx’s value (45) from memory back into the%eaxregister. (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-O2optimization would eliminate; at-O0the compiler doesn’t bother.)mov %eax,%esi— movesx’s value into%esi, which is the register for the second argument toprintf. (printf’s arguments are the format string, thenx— soxis 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>isobjdumphelpfully 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%eaxto0. This looks unrelated to your code, but it’s part of the x86-64 calling convention specifically for variadic functions likeprintf:%eaxmust hold the number of vector/floating-point registers used for the call (0here, 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 becauseprintflives in the dynamically-linked C library (libc), not in this binary itself; the PLT resolves the real address ofprintfat load time (or on first call).
print_binary((unsigned int) x);
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
xfrom 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-byteintas a 4-byteunsigned intinvolves 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_binaryis 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 howmain’sreturn 0becomes the process’s exit status.leave— a shorthand instruction that undoes the prologue in one step: restores%rspfrom%rbp(deallocating the stack frame) and pops the saved%rbpback (undoing the very firstpush %rbp). This is the function epilogue.ret— pops the return address (pushed automatically by thecallinstruction 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)—xarrived in%ediper the calling convention (it’s the argument register). This instruction immediately copies it out intox’s own stack slot, at-0x14(%rbp). This is standard-O0behavior: 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 isi = sizeof(x) * CHAR_BIT - 1, fully computed at compile time.0x1f=31decimal. The compiler already knowssizeof(unsigned int)is 4 andCHAR_BITis 8, so4 * 8 - 1 = 31— no runtime multiplication ever happens; the compiler folded the whole expression into a single constant.ilives at-0x4(%rbp), a separate stack slot fromx’s-0x14(%rbp).jmp 11ab— jumps straight down to the loop’s condition check (address0x11ab, shown further below), skipping the loop body on this first pass through. This is a very characteristic pattern for compiledforloops: 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 yourfor(init; cond; incr) bodyinto 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— loadiinto%eax.mov -0x14(%rbp),%edx— loadxinto%edx.mov %eax,%ecx— copyiinto%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%clregister (the low byte of%ecx) — there’s no shift-by-any-register form. The compiler is forced to shuffleiinto%ecxpurely to satisfy this instruction encoding rule.shr %cl,%edx— the actual shift!shr= shift right, logical (zero-fill) — matchesunsigned int x, confirming exactly the logical-vs-arithmetic distinction from your earlier question: becausexisunsigned, the compiler emitsshr, notsar. This computesx >> 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%eaxwith 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 lasttestfound zero) — jump down to the'0'branch. This is the compiled form of your ternary’sfalsepath.mov $0x31,%eax/jmp 11a0—0x31is ASCII'1'(decimal 49). This is the ternary’struebranch: load the character'1', then jump past the other branch.mov $0x30,%eax(at label119b) —0x30is ASCII'0'(decimal 48) — thefalsebranch’s value.mov %eax,%edi/call 1060 <putchar@plt>— same calling convention pattern as before: put the argument (whichever character was selected) into%edi, callputcharthrough the PLT (dynamically linked, likeprintf).
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 fromi’s stack slot).cmpl $0x0,-0x4(%rbp)— comparesiagainst0(sets flags based oni - 0, without storing the subtraction result anywhere — same “compute flags only” idea astestabove).jns 1181— “jump if not sign” — i.e., jump back up to the loop body (address0x1181) if the comparison result was non-negative, meaningi >= 0still holds. This single instruction is the entire compiled form of youri >= 0loop condition. Onceibecomes-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—0x0ais 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>— sameputcharcall 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 asmain.
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 - 1never executes as a multiplication and subtraction at runtime — it’s baked into a singlemovl $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 formain(in this binary) to callprintf(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(notsar) appears becausexis declaredunsigned— direct, physical confirmation of the logical-vs-arithmetic shift discussion: changeunsigned int xtoint xin the function signature and recompile, and you’d seesaremitted instead for the exact same source-level>>operator.-O0is verbose and redundant on purpose — notice how often a value gets stored to memory and immediately reloaded (e.g.xis 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-O0is the right choice for a tutorial like this one, and-O2is 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
| Op | Name | Rule (per bit pair a,b) |
|---|---|---|
& | AND | 1 only if both are 1 |
| | OR | 1 if either is 1 |
^ | XOR | 1 if they differ |
~ | NOT | flips the single bit |
<< | left shift | slides bits toward higher value (×2 per shift) |
>> | right shift | slides 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:
| Op | mask bit = 1 means… | mask bit = 0 means… |
|---|---|---|
x & mask | keep that bit of x unchanged | force to 0 |
x | mask | force to 1 | keep that bit of x unchanged |
x ^ mask | flip that bit of x | keep that bit of x unchanged |
x & ~mask | force to 0 | keep 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 isolate | Mask |
|---|---|
| last byte | 0xFF |
| last 2 bytes | 0xFFFF |
| last nibble (4 bits) | 0xF |
bit i only | 1 << 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 with0s on the left. Well-defined, no ambiguity. - Right shift on signed, negative
x: implementation-defined — in practice almost universally arithmetic (sign-extends, fills with1s) 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 << 32on a 32-bitint). 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 asx & (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, return0/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
intis implementation-defined, not guaranteed zero-fill. Cast tounsignedfirst if you need guaranteed logical-shift behavior. - Mask width mismatches:
x & 0xFFon a 64-bitxstill 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):
| Expression | Result | Why |
|---|---|---|
x & 0xFF | 0x00000021 | keep last byte, zero rest |
x | 0xFF | 0x876543FF | force last byte to all 1s |
x ^ 0xFF | 0x876543DE | flip only the last byte |
~x | 0x789ABCDE | flip everything |
~x ^ 0xFF | 0x789ABC21 | flip everything, then un-flip last byte |
x << 4 | 0x76543210 | shift left 4, drop top nibble, zero-fill right |
x >> 4 (unsigned) | 0x08765432 | shift 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
forgejoerrors - check service logs with
journalctl -u forgejo -n 50 --no-pager - check
/etc/forgejo/app.inifor 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
sqliteas it is more than enough for me butmysqlandpostgresare alsow available. - change SSH port from
22to2222. - 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:
objdumpstraceltraceftraceobjcopyreadelf
Useful devices and files:
/proc/<pid>/maps/proc/kcore/boot/System.map/proc/kallsyms/proc/iomem
Linker related environment variables:
LD_PRELOADLD_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:
| Expression | Meaning |
|---|---|
*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
constto 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
| Type | Declaration | Specifics |
|---|---|---|
| Character Array | char 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 Pointer | char *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 |