<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: lostghost</title>
    <description>The latest articles on Forem by lostghost (@lostghost).</description>
    <link>https://forem.com/lostghost</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3045125%2F55db0a00-28d3-4dbb-87f6-4d5494ccfca8.png</url>
      <title>Forem: lostghost</title>
      <link>https://forem.com/lostghost</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lostghost"/>
    <language>en</language>
    <item>
      <title>Distributed Applications</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Tue, 28 Oct 2025 05:48:16 +0000</pubDate>
      <link>https://forem.com/lostghost/distributed-applications-4p87</link>
      <guid>https://forem.com/lostghost/distributed-applications-4p87</guid>
      <description>&lt;p&gt;This is a blog series on distributed applications - what goes into them, from start to finish. I spend less time recounting theory - instead prefferring to link to it - and more time describing my takeaways. Hope you enjoy it, and please leave feedback.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/lostghost/distributed-applications-part-1-overview-6g8"&gt;Part 1&lt;/a&gt; - Overview&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/lostghost/distributed-applications-part-15-api-design-36al"&gt;Part 1.5&lt;/a&gt; - Network API&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/lostghost/distributed-applications-part-2-distributed-design-patterns-2f38"&gt;Part 2&lt;/a&gt; - Distributed Design Patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/lostghost/distributed-applications-part-3-distributed-state-11af"&gt;Part 3&lt;/a&gt; - Distributed State&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed the series, consider also my series on &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;Linux&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>distributedsystems</category>
      <category>networking</category>
      <category>database</category>
    </item>
    <item>
      <title>Distributed Applications. Part 3 - Distributed State</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Mon, 27 Oct 2025 07:11:14 +0000</pubDate>
      <link>https://forem.com/lostghost/distributed-applications-part-3-distributed-state-11af</link>
      <guid>https://forem.com/lostghost/distributed-applications-part-3-distributed-state-11af</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/distributed-applications-4p87"&gt;series&lt;/a&gt; on designing distributed applications. In this chapter, we look at distributed state - and the applications that manage it, databases. We won't go too deep into the individual products, and instead try to keep the discussion about the tradeoffs of particular approaches.&lt;/p&gt;

&lt;p&gt;The simplest database is a file (or a block device). You can &lt;a href="https://man7.org/linux/man-pages/man2/lseek.2.html" rel="noopener noreferrer"&gt;seek&lt;/a&gt; around and &lt;a href="https://man7.org/linux/man-pages/man2/write.2.html" rel="noopener noreferrer"&gt;write&lt;/a&gt; to it, or you can &lt;a href="https://man7.org/linux/man-pages/man2/mmap.2.html" rel="noopener noreferrer"&gt;mmap&lt;/a&gt; it and write to memory.&lt;/p&gt;

&lt;p&gt;What are the issues with using a file as a database? Lack of a networked API - you can't expose a file on a TCP port, you need an intermediary (which is a shame, linux has all kinds of low-level file oriented primitives - why not this one?), lack of granular concurrency - you can only lock the entire file, and lack of high-availability - a file can't natively be replicated or sharded.&lt;/p&gt;

&lt;p&gt;We discussed network APIs in &lt;a href="https://dev.to/lostghost/distributed-applications-part-15-api-design-36al"&gt;part 1.5&lt;/a&gt;, so let's look at consensus and concurrency control. Reliable writes to a cluster can be leaderful, or leaderless. Leadership can be with manual failover - like in Postgres, or automatic, with elections - like in Paxos and Raft. I suggest watching the &lt;a href="https://youtu.be/vYp4LYbnnW8" rel="noopener noreferrer"&gt;original presentation&lt;/a&gt; on Raft, it's really insightful.&lt;/p&gt;

&lt;p&gt;Leaderful means that all writes have to go through the leader. That prevents scaling, but helps consistency - Postgres being a good example. The way you can achieve scaling with leaders, is by sharding - have portions of data controlled by independent processes, and thus have different leaders. But you sacrifice consistency between shards - and you get Cassandra LightWeight Transactions - &lt;a href="https://cassandra.apache.org/doc/latest/cassandra/architecture/guarantees.html#lightweight-transactions-with-linearizable-consistency" rel="noopener noreferrer"&gt;docs&lt;/a&gt;. They only work within one shard. As for Postgres - sharding an individual Postgres instance is awkward, so why not run multiple Postgres, and shard that way? What you get is &lt;a href="https://www.citusdata.com/blog/2023/08/04/understanding-partitioning-and-sharding-in-postgres-and-citus/" rel="noopener noreferrer"&gt;Citrus&lt;/a&gt;, &lt;a href="https://cloudberry.apache.org/" rel="noopener noreferrer"&gt;Apache Cloudberry&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But what if you don't want leader-per-partition, but instead true multi-master writing? You will inevitably get conflicting updates, and you have to decide between merging, and Last Writer Wins. Merging can be built-in - like with CRDTs, which you should check out (for example, &lt;a href="https://youtu.be/x7drE24geUw" rel="noopener noreferrer"&gt;here&lt;/a&gt;). But CRDTs are not general-purpose. Conflicts can be exposed to the user - like with Git, which is a kind of database. Or you can just do Last Writer Wins - which is what Cassandra does by default.&lt;/p&gt;

&lt;p&gt;Raft and Paxos give us atomic compare-and-swap within a shard - this can be extended to full serializability, since any reads and writes can be made atomic under a single leader. The mechanism would be &lt;a href="https://en.wikipedia.org/wiki/Two-phase_locking" rel="noopener noreferrer"&gt;2PL&lt;/a&gt;, or &lt;a href="https://www.postgresql.org/docs/current/mvcc.html" rel="noopener noreferrer"&gt;MVCC&lt;/a&gt; with &lt;a href="https://wiki.postgresql.org/wiki/SSI" rel="noopener noreferrer"&gt;SSI&lt;/a&gt; extension. But what about across shards, with different leaders? Well, we can run one more Paxos or Raft algorithm, but this time over these shards - who then have their own Paxos or Raft groups, and achieve consensus. This is a lot of round-trips - so the fewer shards are involved in a transaction, the better. The paper on distributed commit with Paxos is &lt;a href="https://lamport.azurewebsites.net/video/consensus-on-transaction-commit.pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is much better than the established two-phase-commit protocol, &lt;a href="https://en.wikipedia.org/wiki/Two-phase_commit_protocol" rel="noopener noreferrer"&gt;2PC&lt;/a&gt;. It is a lot like state machine replication in Raft - the leader sends updates, quorum confirms receiving them, leader sends "commit" - except, 2PC operates only one replica of the coordinator process, so  if the leader fails - there is no automatic reelection, everyone just waits for leader to come back alive. To me, Raft/Paxos looks strictly better. In fact, if you take 2PC, run multiple processes that come to a quorum, and make it do leader reelection - it starts to look a lot like Raft.&lt;/p&gt;

&lt;p&gt;I mentioned 2PL, Two Phase Locking, and MVCC - Multi-Version Concurrency Control. These are the two main concurrency control system within a shard. 2PC establishes locks for readers and writes - making the best effort to avoid blocking, while MVCC creates snapshots of state for each transaction - which avoid all blocking, but need garbage collection afterwards. MVCC is generally faster, especially for transactions with relaxed isolation level of "Snapshot isolation" - ideal for read-only transactions, that simply need a consistent view of data. Using snapshot isolation for writing transactions can lead to &lt;a href="https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads" rel="noopener noreferrer"&gt;write skew&lt;/a&gt; - so to be safe, one should stick to serializable writing transactions.&lt;/p&gt;

&lt;p&gt;And lastly, there is one more subject to address - the interaction between application-level transactions and database-level transactions. You should watch &lt;a href="https://www.youtube.com/watch?v=5ZjhNTM8XU8" rel="noopener noreferrer"&gt;this&lt;/a&gt; video for full context. Basically, with every application having its own database, and following the saga pattern - they implement their own distributed transactions, just at the application level. This is not ideal, since these implementations are improvised, and prone to errors. But we don't want to do distributed transactions across the whole internet either - not with 2PC certainly, we can't lock the world, to wait for decisions from across the Internet. The real world is not consistent - if we are to model it, we need to deal with that fact.&lt;/p&gt;

&lt;p&gt;So what's the solution? We want distributed state to be handled by a dedicated system, making application logic as pure as possible (doesn't matter if it's a functional or object-oriented language - both can be modeled with pure computation in mind, and be equivalent). And we want distributed transactions. If we have 2PC across the internet - this won't scale. We need to use Paxos Commit, or equivalents. Furthermore, current serializability enforcement is too coarse - it only checks, which rows were read and written, but not the contents, not business constraints. For example, consider that an entity/object/actor of "account" has the business constraint of being non-negative. Multiple long-lived transactions across services also decrement the balance, checking that it's non-negative. If the DB only checks the fact of reading and writing - these transactions would cancel each other, despite not actually violating the constraint. This doesn't scale.&lt;/p&gt;

&lt;p&gt;Let's imagine a world with distributed transactions, that do actually check business constraints. They could still cancel each other at unpredictable times. This is fine, except - applications are written under different assumptions. The source of truth is in-process state, which gets reflected into a database. A workflow has a notion of forward-progress - and cannot be rolled back several steps, if the distributed transaction gets cancelled. What I'm proposing is a different model - where the source of truth is the database, and application code is a pure function, making no assumptions about monotonic time, and instead gets driven entirely by the database. Under that paradigm, distributed transactions can scale. This is very similar to the &lt;a href="https://en.wikipedia.org/wiki/Actor_model" rel="noopener noreferrer"&gt;actor model&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for reading this post! Next one will be about practical design of a distributed system.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>database</category>
    </item>
    <item>
      <title>Distributed Applications. Part 1.5 - Network API</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Sun, 26 Oct 2025 14:22:31 +0000</pubDate>
      <link>https://forem.com/lostghost/distributed-applications-part-15-api-design-36al</link>
      <guid>https://forem.com/lostghost/distributed-applications-part-15-api-design-36al</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/distributed-applications-4p87"&gt;series&lt;/a&gt; on designing distributed applications. In this chapter, we will examine different technologies and approaches for a network API.&lt;/p&gt;

&lt;p&gt;We can split the different approaches to a network API into the following categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary over TCP/UDP&lt;/li&gt;
&lt;li&gt;Text over TCP/UDP&lt;/li&gt;
&lt;li&gt;JSON over HTTP&lt;/li&gt;
&lt;li&gt;gRPC&lt;/li&gt;
&lt;li&gt;GraphQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Binary over a socket is a popular choice - Postgres, MySQL, MSSQL, Cassandra, Redis, Mongo, CockroachDB use it. You can tailor the protocol to your use-case, and get the most performance out of it. You don't lock yourself into a corporation's technology, that you have no influence over. But there are downsides - with TCP, you lose in performance, so the protocol-level optimization may be not worth it. With UDP you have to reinvent the wheel - using &lt;a href="https://en.wikipedia.org/wiki/QUIC" rel="noopener noreferrer"&gt;QUIC&lt;/a&gt; would be a good choice. You then need to evolve your protocol and version it, as well as all of the custom tools and drivers for it. &lt;/p&gt;

&lt;p&gt;Text presents a lower barrier for tooling - but it is less efficient. And ultimately, you will be transmitting binary data - so you either base64 encode it, bloating its size, or you leave it as is, making the output of your protocol unreadable. Or you use two pairs of sockets - one for "control-plane" text-only queries, and one for binary data. A potential footgun is with firewalls, as with &lt;a href="https://ncftp.com/ncftpd/doc/misc/ftp_and_firewalls.html" rel="noopener noreferrer"&gt;FTP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;JSON over HTTP has much more infrastructure already in place, but it is ultimately still text over TCP - and all the problems that come with it. Afterall, there is a reason MongoDB uses BSON - a binary protocol.&lt;/p&gt;

&lt;p&gt;gRPC solves that problem, and was basically imagined as a binary JSON over HTTP - with a built-in schema. But there are issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is controlled by Google, and your needs should align with Google's needs&lt;/li&gt;
&lt;li&gt;You have to use ProtoBuf for schema definition and, most often, binary encoding. Most tools assume ProtoBuf on the wire&lt;/li&gt;
&lt;li&gt;It demands HTTP/2, even though HTTP/3 is available. Google should get around to adopting HTTP/3 at some point, but it doesn't look like they are in a hurry.&lt;/li&gt;
&lt;li&gt;It is not natively supported in the browser. And proxies offer a limited functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And finally, GraphQL. It is a powerful query language - much like SQL. But exposing SQL to a client is challenging - databases are not built for that. ACLs are hard, versioned APIs are hard, rate limiting is hard. Some of it can be solved by proxies, such as Envoy and HAProxy, but not all. And you will still want an internal representation, and a public API - may as well use a different technology. But the issues with GraphQL are the same - ACLs are hard, versioned APIs are hard, rate limiting is hard. I don't think it's a good choice for those outside Meta (it is not an independent project). Read more &lt;a href="https://bessey.dev/blog/2024/05/24/why-im-over-graphql/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is also the option of a language-level RPC library, such as &lt;a href="https://trpc.io/" rel="noopener noreferrer"&gt;tRPC&lt;/a&gt; or &lt;a href="https://rpyc.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;RPyC&lt;/a&gt;. The former is a great option, if your code is Javascript/Typescript on both frontend and backend, with a bonus if your team is full-stack. If you can force the clients to upgrade, and you have dedicated APIs for every client - you can change them at will, with no versioning, no coordination. As for RPyC, because it works on object proxies - if the server or the client restarts, all the proxies break - and if they are all over your state, the whole system has to restart.&lt;/p&gt;

&lt;p&gt;So what do we do then? I'd stick with JSON over HTTP, based on &lt;a href="https://docs.bump.sh/guides/openapi/specification/v3.2/introduction/what-is-openapi/" rel="noopener noreferrer"&gt;OpenAPI&lt;/a&gt; spec. Coupled with tools for code-generation and input (and output) validation based on the spec, this is a good universal solution - a web standard, available on all platforms, and not tied to any company. JSON is not as efficient as binary, even when gzipped - that is a tradeoff.&lt;/p&gt;

&lt;p&gt;But there is a larger takeaway. Say you have one backend, and many clients - browser, phone, desktop. You can create dedicated APIs for every client - and, if upgrades are mandated, not version them, not coordinate with anyone, just deploy new APIs. To me, this looks close to optimal. Another option is to have a universal JSON API for all clients. But clients have different needs, and may be developed by different teams - you need versioning. And it will be hard to avoid overfetching and underfetching. You may then want to introduce a query language - but you will just be reinventing GraphQL, with all its problems. But if you want the most universal API, which can integrate with any possible application - you are looking at the "Semantic Web" protocol suite. It is worth looking at - here are a &lt;a href="https://www.youtube.com/watch?v=HSFoxYC169o" rel="noopener noreferrer"&gt;couple&lt;/a&gt; &lt;a href="https://www.youtube.com/watch?v=jfb04V8WY6Y" rel="noopener noreferrer"&gt;resources&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is it, when it comes to networked API design - thank you for reading!&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Distributed Applications. Part 2 - Distributed Design Patterns</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Fri, 17 Oct 2025 17:30:14 +0000</pubDate>
      <link>https://forem.com/lostghost/distributed-applications-part-2-distributed-design-patterns-2f38</link>
      <guid>https://forem.com/lostghost/distributed-applications-part-2-distributed-design-patterns-2f38</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/distributed-applications-4p87"&gt;series&lt;/a&gt; on designing distributed applications. In this chapter, we will examine the different design patterns, commonly employed in the industry for distributed applications.&lt;/p&gt;

&lt;p&gt;First let's address a widely heard, but poorly understood concept - microservices. What is a microservice, and how is it useful? For some reason, the term came to mean "your program should be many small programs, which send JSON over HTTP". What makes a small program? Why JSON and HTTP specifically? Seems like some sort of cargo cult.&lt;/p&gt;

&lt;p&gt;A microservice is a service owned as completely as possible by one team, that can be deployed without coordination with any other team. Microservices are about office politics (namely, &lt;a href="https://en.wikipedia.org/wiki/Conway's_law" rel="noopener noreferrer"&gt;Conway's law&lt;/a&gt;), and not any particular technical solution.&lt;/p&gt;

&lt;p&gt;With that out of the way, let's discuss CQRS, a widely adopted and useful pattern. CQRS stands for "Command-Query Responsibility Segregation", and it means that interations with a software system should be separated into commands and queries. Commands change the state of the system, and produce no results, while queries produce results, but don't change the state. This has many implications.&lt;/p&gt;

&lt;p&gt;Commands require special handling - once accepted, they have to be processed, and in doing so, their effect should only be applied once. The first part means that a command is durably recorded under a unique ID, the second part means that a command is either carefully managed under that ID, or is &lt;a href="https://en.wikipedia.org/wiki/Idempotence" rel="noopener noreferrer"&gt;idempotent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Queries require much less handling - they don't need to be recorded and then executed, they can be executed right away, and have their results returned on the same connection. If the connection breaks - they can be retried with no worries, since they are idempotent by their nature.&lt;/p&gt;

&lt;p&gt;And lastly, both commands and queries are in themselves stateless. State lives in a higher-level container called "workflow", consisting of commands and queries, that is responsible for the entire business-relevant operation, and exists either in a dedicated piece of software, or is scattered across different services.&lt;/p&gt;

&lt;p&gt;Let us now go over widespread communication patterns. They aren't as relevant, if you are only making the "stateless compute" part of a distributed system yourself, which requires no coordination among itself, and rely on the database for all state handling. But if you do need reliable communication over the network, how do you arrange for it?&lt;/p&gt;

&lt;p&gt;Direct &lt;a href="https://en.wikipedia.org/wiki/Remote_procedure_call" rel="noopener noreferrer"&gt;RPC&lt;/a&gt; is a good option - with your own framing over TCP, or even your own reliability over UDP. Maybe JSON over HTTP. Either way, you decide if state lives only within one session - then you can put the other end behind a load balancer, but you lose the benefit of data locality - or if state lives between sessions - then you need to expose all IPs and ports, and worry about correct failover.&lt;/p&gt;

&lt;p&gt;But direct RPC requires a degree of availability - at least one instance of every service should be always available. If that constraint isn't met, you need a sort of "socket activation" intermediary, to hold on to messages. Further, with direct RPC, every service needs to durably record commands that were sent, to their respective database - what if they were recorded in a shared database? This brings us to &lt;a href="https://en.wikipedia.org/wiki/Message-oriented_middleware" rel="noopener noreferrer"&gt;queues and message brokers&lt;/a&gt;. They aren't strictly better than direct RPC, and create a single point of failure, but they are a solid option, widely adopted in the industry. &lt;/p&gt;

&lt;p&gt;Both direct RPC and queues commonly have at-least-once delivery guarantees - if reception of a message could not be confirmed, the message will be resent. This means, commands need to be idempotent.&lt;/p&gt;

&lt;p&gt;Let's now scale this out to multiple independent services (possibly owned and operated by different teams). What if you need to perform an operation, that spans multiple services? Check for a condition in two services, and then perform a command on two services. But there can be a race condition - while you are checking the second service, the condition in the first service may become false. You need a transaction. There are two options.&lt;/p&gt;

&lt;p&gt;First is a distributed transaction, with a transaction manager, and the &lt;a href="https://en.wikipedia.org/wiki/X/Open_XA" rel="noopener noreferrer"&gt;OpenXA&lt;/a&gt; protocol, with &lt;a href="https://en.wikipedia.org/wiki/Two-phase_commit_protocol" rel="noopener noreferrer"&gt;two-phase commit&lt;/a&gt;. Issue is that if you have no control over a third-party service - you can't force them to adopt the transaction manager. And they may not want to let you lock their database.&lt;/p&gt;

&lt;p&gt;The second option is the &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;Saga pattern&lt;/a&gt;. Check the condition on the first service, perform an operation, and check the condition again. If the result is not what you want - rollback the operation, with a compensating operation, that should be paired with any positive operation. You no longer need to stop the world to coordinate different services - letting go of the illusion of control gives better performance.&lt;/p&gt;

&lt;p&gt;Issue is, if you have a complex business process, that spans multiple services - the code of it may also get scattered across those services, and the whole process will be hard to make sense of, especially when things go wrong. One thing you should consider doing, is centralised logging and tracing, to follow the request between services. Second, factor out the process into a separate service, so the code is all in one place - keep the other services dumb. Third - you may want to have a centralised dashboard for logs and traces for all operations, with the option of canceling or restarting any operation - a workflow engine like Temporal can help in this case. Fourth, you may want to allow non-technical people to program and observe workflows at a high-level - a &lt;a href="https://en.wikipedia.org/wiki/Business_Process_Model_and_Notation" rel="noopener noreferrer"&gt;BPMN&lt;/a&gt; engine like Camunda and Flowable is a good option.&lt;/p&gt;

&lt;p&gt;Another pattern is &lt;a href="https://en.wikipedia.org/wiki/Eventual_consistency" rel="noopener noreferrer"&gt;eventual consistency&lt;/a&gt;. Programs rely on forward progress, on monotonically increasing time and instruction pointer. Programs shouldn't "jump backwards" - otherwise logic, and cause and effect breaks. This won't happen as much, if you have stateful and sticky sessions - the same node will be receiving reads and writes. But what if you use stateless backends behind a loadbalancer, and separate read replicas? It may take time for writes to propagate to where you're reading from - so you won't see the effects of your own commands. There are two options - either specify "wait for propagation" in all of your commands, or accept eventual consistency, by the way of versioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give every entity a version&lt;/li&gt;
&lt;li&gt;Show the user the entity, remember its version N&lt;/li&gt;
&lt;li&gt;If the user updates the entity - specify in the command "if version is still N", and additionally increment the version (in the same command, atomically)&lt;/li&gt;
&lt;li&gt;If the version is no longer N - it was updated by somebody else - either check with business logic that the update still makes sense, or ask the user again, based on the new data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many databases allow you to specify conditions in the update - if you know that the user made an update based on not the whole data, but specific conditions - you can avoid versioning, and include the conditions in the command (if the price is still under 100$, order the phone - no need to ask the user again because the price changed, since the price went down)&lt;/p&gt;

&lt;p&gt;Another thing to consider is the sharding of your backend. &lt;/p&gt;

&lt;p&gt;At a high level, the unit of sharding is either user request, or application entity. Per user request means that your entities are passive stores of data - and coordination of their manipulation will require locking. But if application entities are living participants in the communication, you get the &lt;a href="https://en.wikipedia.org/wiki/Actor_model" rel="noopener noreferrer"&gt;actor model&lt;/a&gt; - which does not need locks.&lt;/p&gt;

&lt;p&gt;At the low level, your application can be single-threaded, multi-process, multi-threaded, async with coroutines, async with virtual threads. Consider that Linux tracks memory (and performs OOM kill) per-process, and that async coroutines introduce function coloring.&lt;/p&gt;

&lt;p&gt;And lastly, &lt;a href="https://microservices.io/patterns/data/event-sourcing.html" rel="noopener noreferrer"&gt;Event Sourcing&lt;/a&gt;. Event Sourcing is the observation that the state of your application is the result of processing a series of inputs, and the conclusion of separating the state into "source of truth" which is the append-only event log, and many "read-friendly" representations of it. &lt;a href="https://en.wikipedia.org/wiki/Journaling_file_system" rel="noopener noreferrer"&gt;Journaled file-systems&lt;/a&gt;, journaled databases are examples of event sourcing - sending the database &lt;a href="https://en.wikipedia.org/wiki/Write-ahead_logging" rel="noopener noreferrer"&gt;WAL file&lt;/a&gt; to a separate analytical database is the example of creating a new read-friendly representation for a particular need. The downside is that you need to evolve (and version) multiple sets of entities - the ones in the event log, and the ones in every read representation - this can quickly become overwhelming. The upside is that you can destroy any read-friendly representation - it can always be recreated from the event log.&lt;/p&gt;

&lt;p&gt;This concludes our exploration of application-level design patterns for distributed systems. Let me know if I missed any! In the next blog we will discuss the internal organisation and tradeoffs of different databases. Thank for reading&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>designpatterns</category>
      <category>microservices</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Distributed Applications. Part 1 - Overview</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Thu, 16 Oct 2025 10:55:56 +0000</pubDate>
      <link>https://forem.com/lostghost/distributed-applications-part-1-overview-6g8</link>
      <guid>https://forem.com/lostghost/distributed-applications-part-1-overview-6g8</guid>
      <description>&lt;p&gt;In this blog &lt;a href="https://dev.to/lostghost/distributed-applications-4p87"&gt;series&lt;/a&gt; we will examine distributed systems - and the modern practices associated with them. This will not be an academic series, but a practical one - there won't be citations of any papers, only evaluation of software products, many of which were originally based on a paper in a scientific journal. As for literature, I recommend "Designing Data-Intensive Applications" by Martin Kleppmann. He has a &lt;a href="https://www.youtube.com/@kleppmann" rel="noopener noreferrer"&gt;youtube channel&lt;/a&gt;, and a &lt;a href="https://www.youtube.com/playlist?list=PLeKd45zvjcDFUEv_ohr_HdUFe97RItdiB" rel="noopener noreferrer"&gt;lecture series&lt;/a&gt; on distributed systems. He also gives many &lt;a href="https://www.youtube.com/playlist?list=PLeKd45zvjcDHJxge6VtYUAbYnvd_VNQCx" rel="noopener noreferrer"&gt;talks&lt;/a&gt;. There also is a &lt;a href="https://www.distributed-systems.net/index.php/books/ds4/ds4-ebook/" rel="noopener noreferrer"&gt;book&lt;/a&gt; by the titan Andrew Tanenbaum - you can get it for &lt;em&gt;free&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So what is a distributed system? The accepted definition is - a software system with components on different networked computers. Now why is the networked part important, why can't threads or processes on the same computer form a distributed system? Because the network is unreliable, compared to memory. It could also be slower than disk, if you add up all the overhead - modern NVMe is ~7GB/s, 100Gb Ethernet is ~12.5GB/s&lt;/p&gt;

&lt;p&gt;This unreliable network introduces the core challenges faced by distributed systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's hard to know if your message was processed by the recipient&lt;/li&gt;
&lt;li&gt;A netsplit splits a shared understanding of state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is on top of challenges faced by a concurrent program - a good distributed program is also a good concurrent program. The distributed solution is more general.&lt;/p&gt;

&lt;p&gt;A separate, but related issue is horizontal scaling. A system's processing capacity should grow with the amount of hardware that gets added to it - while not getting eaten up by the extra overhead. For stateless compute, this is simple enough - spawn more copies of the application - while for the stateful database, sharding needs to be employed, and that involves careful coordination.&lt;/p&gt;

&lt;p&gt;And finally, fault tolerance. At any moment, there is a chance that your program catches a bug and breaks, or slows down to the point of being unresponcive. If your system consists of a large number of such components - something, somewhere is always broken. And your system needs to tolerate that. This would also make for a good local program - but it's easier for them to crash completely, if anything is broken. Once again, the problem of distributed computing is the more general than local computing.&lt;/p&gt;

&lt;p&gt;And really, the central problem of distributed computing is distributed state - that's why there's so much talk of databases, since they are responsible for it. Each database presents its tradeoffs, which you need to keep in mind for your actual application. This is in addition to problems of local state - namely, the correct modeling of it, and the ability of its evolution - you may want to look into &lt;a href="https://en.wikipedia.org/wiki/Domain-driven_design" rel="noopener noreferrer"&gt;Domain Driven Design&lt;/a&gt; for that.&lt;/p&gt;

&lt;p&gt;What is distributed state? To an individual program, running on an individual computer, there is no local or distributed state, there is only state - the only one it has. Much like to a person, there is no subjective or objective reality, there is only reality - the one they experience. Objective reality, much like distributed state, is a contrivance - it is simply whatever the individual participants can agree on. This means that for distributed state, you need an algorithm - one that, within pre-determined failure conditions, can make it so individual participants can always agree on an objective reality, on distributed state.&lt;/p&gt;

&lt;p&gt;Say there is a netsplit - 5 out of 9 nodes recorded an operation, while 4 out of 9 didn't, and they can't reach each other. What is the reality, did the operation happen? That is, once again, determined by the algorithm, which is usually based on a rule of thumb - if one node is unavailable, that's most likely because that node is broken, if many nodes are unavailable - the majority decides what the distributed state is, because the majority is more fault-tolerant, due to having more nodes. But deciding that in the moment, there is no valid shared state, that we should stop accepting operations and wait for the other nodes to become available again, is also a valid design - you are trading availability for consistency, which is the dilemma posed by the &lt;a href="https://en.wikipedia.org/wiki/CAP_theorem" rel="noopener noreferrer"&gt;CAP theorem&lt;/a&gt;. A possible solution is replication - if the remaining 5 nodes can assume ownership over entities previously owned by the other 4 nodes, because they have all the necessary data - the system can continue accepting operations. The 4 nodes would need to realise that they are in the minority, stop accepting operations based on stale data, catch up on the data, once available, and only then accept operations again.&lt;/p&gt;

&lt;p&gt;Another common fault is a transient network outage - a message gets lost, and a sender gets no reply. Should the sender send the message again, at the risk of potentially doing the same thing twice? (like removing money from someone's bank account, which you don't want to do twice by accident). This creates at-least-once and at-most-once delivery guarantees for messaging frameworks, which one also needs to design around.&lt;/p&gt;

&lt;p&gt;To summarise, regular applications need to correctly model internal state, based on the real world, and make operations on that state, which are useful to the business. Distributed applications then need an algorithm to agree on shared state, and in doing so, decide on the tradeoff between consistency and availability, in the event of outages and netsplits.&lt;/p&gt;

&lt;p&gt;In the following blog, we will examine the common patterns employed by applications, to deal with distributed state. Thank you for reading!&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>programming</category>
    </item>
    <item>
      <title>Linux from the developer's perspective. Part 4 - strace and pmap</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Sun, 20 Jul 2025 13:50:12 +0000</pubDate>
      <link>https://forem.com/lostghost/linux-from-the-developers-perspective-part-4-strace-and-pmap-1bph</link>
      <guid>https://forem.com/lostghost/linux-from-the-developers-perspective-part-4-strace-and-pmap-1bph</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Getting the program to compile and run is a challenge. Getting it to run correctly is even more of a challenge. You would want to know exactly what the program is doing, and how it's different from what you intended for it to do. This is known as debugging.&lt;/p&gt;

&lt;p&gt;Debugging takes place at various stages of a program's lifecycle, starting from the programming stage. There are various linters and scanners that go over your source code and identify undesireable functionality. Most of them are intended for use from your editor-turned-IDE, such as VIm. As an example, for the programs &lt;code&gt;ctags&lt;/code&gt; and &lt;code&gt;cscope&lt;/code&gt;, here's a &lt;a href="https://youtu.be/9NcM-Tj2UZI" rel="noopener noreferrer"&gt;video&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After programming and compilation, you get a binary program image, which you can also analyse. Analysis at both source code and binary image stages is called static analysis. We already did static analysis, when going over program segments with &lt;code&gt;readelf&lt;/code&gt;. A popular static analysis tool is &lt;a href="https://valgrind.org/" rel="noopener noreferrer"&gt;valgrind&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now let's turn to dynamic analysis. Because a userspace process is a virtualized environment, any interaction with the outside world, any side-effect, has to go through the kernel, in the form of a syscall. So to get an idea for what the program is really doing, monitoring system calls is a good idea. &lt;code&gt;strace&lt;/code&gt; helps with that.&lt;/p&gt;

&lt;p&gt;Take the same program from the previous blog, compiled statically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ cat main.c 
#include &amp;lt;stdio.h&amp;gt;
int main(int argc, char** argv){
    if (argc&amp;lt;2) return 1;
    printf("%s\n",argv[1]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And let's see what it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ strace ./a.out 
execve("./a.out", ["./a.out"], 0x7ffeb1ab5720 /* 41 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x405658)       = 0
set_tid_address(0x405790)               = 1631947
exit_group(1)                           = ?
+++ exited with 1 +++
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, it &lt;code&gt;exec&lt;/code&gt;s the program. Then it sets the &lt;code&gt;FS&lt;/code&gt; register, used for pointing to thread-local variables. Then it sets the &lt;code&gt;TID&lt;/code&gt; - thread ID - address, used for multithreading. Finally, it exits with code zero.&lt;br&gt;
And now with an argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ strace ./a.out hello
execve("./a.out", ["./a.out", "hello"], 0x7ffdf3a92938 /* 41 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x405658)       = 0
set_tid_address(0x405790)               = 1632018
ioctl(1, TIOCGWINSZ, {ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0}) = 0
writev(1, [{iov_base="hello", iov_len=5}, {iov_base="\n", iov_len=1}], 2hello
) = 6
exit_group(0)                           = ?
+++ exited with 0 +++
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides what we already went over, it asks for the size of the terminal - 80x24 in this case, and writes "hello" to the terminal - the "2hello" is it writing out "hello" mid-output from strace. It then exits successfully with code 0.&lt;/p&gt;

&lt;p&gt;Now let's compile our executable dynamically. To get the full trace, run it the following way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ strace -f /lib/ld-musl-x86_64.so.1 ./a.out hello

execve("/lib/ld-musl-x86_64.so.1", ["/lib/ld-musl-x86_64.so.1", "./a.out", "hello"], 0x7ffd9a5ff938 /* 41 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x740a78ee5b68) = 0
set_tid_address(0x740a78ee5fd0)         = 1632581
open("./a.out", O_RDONLY|O_LARGEFILE)   = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0&amp;gt;\0\1\0\0\0@\20\0\0\0\0\0\0"..., 960) = 960
mmap(NULL, 20480, PROT_READ, MAP_PRIVATE, 3, 0) = 0x740a78e38000
mmap(0x740a78e39000, 4096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 3, 0x1000) = 0x740a78e39000
mmap(0x740a78e3a000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED, 3, 0x2000) = 0x740a78e3a000
mmap(0x740a78e3b000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x2000) = 0x740a78e3b000
close(3)                                = 0
brk(NULL)                               = 0x5555572c6000
brk(0x5555572c8000)                     = 0x5555572c8000
mmap(0x5555572c6000, 4096, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5555572c6000
mprotect(0x740a78ee2000, 4096, PROT_READ) = 0
mprotect(0x740a78e3b000, 4096, PROT_READ) = 0
ioctl(1, TIOCGWINSZ, {ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0}) = 0
writev(1, [{iov_base="hello", iov_len=5}, {iov_base="\n", iov_len=1}], 2hello
) = 6
exit_group(0)                           = ?
+++ exited with 0 +++
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is because:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ file /lib/ld-musl-x86_64.so.1
/lib/ld-musl-x86_64.so.1: symbolic link to /usr/lib/musl/lib/libc.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With musl, the C library is it's own loader! So you don't need to load the loader, then the C library, then the executable - the loader and the library are loaded together (due to being the same file), and only the executable is left. Compare this to glibc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ strace ./a.out 
execve("./a.out", ["./a.out"], 0x7ffc43b5b910 /* 41 vars */) = 0
brk(NULL)                               = 0x5adf33995000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=138995, ...}) = 0
mmap(NULL, 138995, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7894a5b10000
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0&amp;gt;\0\1\0\0\0px\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 840, 64) = 840
fstat(3, {st_mode=S_IFREG|0755, st_size=2006328, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7894a5b0e000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 840, 64) = 840
mmap(NULL, 2030680, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7894a591e000
mmap(0x7894a5942000, 1507328, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x24000) = 0x7894a5942000
mmap(0x7894a5ab2000, 319488, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x194000) = 0x7894a5ab2000
mmap(0x7894a5b00000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e1000) = 0x7894a5b00000
mmap(0x7894a5b06000, 31832, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7894a5b06000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7894a591b000
arch_prctl(ARCH_SET_FS, 0x7894a591b740) = 0
set_tid_address(0x7894a591ba10)         = 1632711
set_robust_list(0x7894a591ba20, 24)     = 0
rseq(0x7894a591b680, 0x20, 0, 0x53053053) = 0
mprotect(0x7894a5b00000, 16384, PROT_READ) = 0
mprotect(0x5adf31a26000, 4096, PROT_READ) = 0
mprotect(0x7894a5b67000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7894a5b10000, 138995)          = 0
exit_group(1)                           = ?
+++ exited with 1 +++
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has to go through the effort of finding the libc first. Let's now compare the actual memory maps. For that, modify the source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ cat main.c 
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

int main() {
    printf("My PID is: %d\n", getpid());
    printf("Press Enter to exit...");
    calloc(2048,2048);
    getchar();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the program in one terminal. In another, run &lt;code&gt;pmap &amp;lt;PID&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux ~]$ pmap 1634216
1634216:   ./a.out
000055aa8a1c4000      4K r---- a.out
000055aa8a1c5000      4K r-x-- a.out
000055aa8a1c6000      4K r---- a.out
000055aa8a1c7000      4K r---- a.out
000055aa8a1c8000      4K rw--- a.out
000055aa8bdb0000    132K rw---   [ anon ]
00007be23b9b4000   4112K rw---   [ anon ]
00007be23bdb8000    144K r---- libc.so.6
00007be23bddc000   1472K r-x-- libc.so.6
00007be23bf4c000    312K r---- libc.so.6
00007be23bf9a000     16K r---- libc.so.6
00007be23bf9e000      8K rw--- libc.so.6
00007be23bfa0000     40K rw---   [ anon ]
00007be23bfcc000      4K r---- ld-linux-x86-64.so.2
00007be23bfcd000    164K r-x-- ld-linux-x86-64.so.2
00007be23bff6000     44K r---- ld-linux-x86-64.so.2
00007be23c001000      8K r---- ld-linux-x86-64.so.2
00007be23c003000      4K rw--- ld-linux-x86-64.so.2
00007be23c004000      4K rw---   [ anon ]
00007ffe8077a000    132K rw---   [ stack ]
00007ffe807b2000     16K r----   [ anon ]
00007ffe807b6000      8K r-x--   [ anon ]
ffffffffff600000      4K --x--   [ anon ]
 total             6644K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that our program takes up 5 pages, with different permissions. The second one is definitely code, or &lt;code&gt;.text&lt;/code&gt; - it has "rx" permissions. The last one, under "rw" - is global variables. The rest are constants, read-only data.&lt;/p&gt;

&lt;p&gt;Then comes the heap - it's the largest element in the output. I made it so by allocating a large amount of memory - so it stands out. It is boxed in by the libc and the program itself. Let's see how much room it has to grow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(0x00007be23bdb8000 - 0x000055aa8bdb0000) / 1024 / 1024/1024/1024
= 38.2175293266773223877
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;38 Terabytes. Should be enough :)&lt;/p&gt;

&lt;p&gt;After the libc and the linker, comes the stack. It has room to grow up til' the fff address. Let's calculate, how much that is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(0xffffffffff600000 - 0x00007ffe8077a000) / 1024 / 1024/1024/1024/1024/1024
= 15.99987793525954060669
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;15 Exabytes. So why is it that a program so easily crashes with "stack overflow", when it has 15 Exabytes of stack?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ ulimit -s
8192
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the answer. The stack is artificially limited, to catch "endless recursion" bugs. If you remove that limit - you won't catch "stack overflow", it will be regular "Out of memory" instead.&lt;/p&gt;

&lt;p&gt;Let's compare this pmap to that of a static executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux ~]$ pmap 1636409
1636409:   ./a.out
0000000000400000      4K r---- a.out
0000000000401000     20K r-x-- a.out
0000000000406000      4K r---- a.out
0000000000407000      8K rw--- a.out
0000000002036000   4096K rw---   [ anon ]
00007ffd8a010000    132K rw---   [ stack ]
00007ffd8a189000     16K r----   [ anon ]
00007ffd8a18d000      8K r-x--   [ anon ]
ffffffffff600000      4K --x--   [ anon ]
 total             4292K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much nicer!&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;pmap&lt;/code&gt; doesn't tell us the full story - only the part that relates to our program. To learn the full story, of the virtual memory mapping for a userspace executable on Linux - refer to this &lt;a href="https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt" rel="noopener noreferrer"&gt;document&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An even better writeup can be found &lt;a href="https://gist.github.com/CMCDragonkai/10ab53654b2aa6ce55c11cfc5b2432a4" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why is it important to know all this? Because dynamic libraries are a security nightmare. If the attacker tricks the executable into loading a malicious library, the executable is compromised, and can execute arbitrary code (with the privileges of the executable - made worse by &lt;a href="https://en.wikipedia.org/wiki/Setuid" rel="noopener noreferrer"&gt;SUID&lt;/a&gt;. How can an executable be tricked into loading a malicious library?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With LD_PRELOAD&lt;/li&gt;
&lt;li&gt;With write access to folders in library search path&lt;/li&gt;
&lt;li&gt;With rpath pointing to a writable directory&lt;/li&gt;
&lt;li&gt;With replacing the libc loader&lt;/li&gt;
&lt;li&gt;With replacing the loader path in a binary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yeah, this is a broken system. Containers help, I guess. But I'd say, getting rid of shared libraries altogether is a good idea. &lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>Wow, everything is graph!</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Sun, 13 Jul 2025 14:57:37 +0000</pubDate>
      <link>https://forem.com/lostghost/wow-everything-is-graph-56oc</link>
      <guid>https://forem.com/lostghost/wow-everything-is-graph-56oc</guid>
      <description>&lt;p&gt;There are two sides to programming, two approaches. &lt;/p&gt;

&lt;p&gt;One approach is low-level. A computer is a collection of ciruits, some more programmable than others. A process sees a virtualized memory layout, which allows it to access devices over a bus - memory being just one of the devices. And what programming is about, is learning how to shove bits down the pipe - as fast as the hardware allows it. Those who appreciate computers, myself included, can see how this view is appealing. It correlates with the imperative/procedural way of describing computations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8dp81j02n392pmkqp7vq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8dp81j02n392pmkqp7vq.png" alt=" " width="768" height="537"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.eetasia.com/debugging-ethernet-sata-and-pcie-for-iot-devices/" rel="noopener noreferrer"&gt;Image source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second approach is mathematical. A program is a transformation of inputs to outputs - it is a function. Functions operate on data - sets, vectors, numbers, sets of numbers. Functions can also depend on their own results - thus you get a finite state machine. There are queue networks, Petri nets - robust mathematical models. This approach is more in line with the functional way of describing computations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxszk298ylvjrum1s33j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxszk298ylvjrum1s33j4.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://en.wikipedia.org/wiki/Vector_addition_system#/media/File:Vector_addition_with_states.svg" rel="noopener noreferrer"&gt;Image source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But there is a third approach, more oriented towards computational systems. In my view, a system is a collection of interacting parts, that together perform a higher-level functionality. And the natural way to represent such a system is with a graph, where each node is also a subgraph. This maps well to the object graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4x2ixsczprkesm6ausa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4x2ixsczprkesm6ausa.png" alt=" " width="735" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://ru.pinterest.com/pin/498421883754670891/" rel="noopener noreferrer"&gt;Image source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The object graph unites two graphs - the execution graph, and the data graph, by the virtue of objects containing both fields and methods. To me, this leads to a natural question - if what we create is a graph, why are we programming text?&lt;/p&gt;

&lt;p&gt;So much of programming complexity is in keeping track of how things relate to each other - and by changing a part of the system, what other parts will be affected. A graph answers that question much better than text - yet so far, it's power was only successfully utilized in quick diagrams on a whiteboard, and BPMN engines. Is the program really that hard to represent as a graph, and also - is it too hard to actually program?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01t5soie9tk24po3zz6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01t5soie9tk24po3zz6f.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dishcuss.com/post/BD1465DC975FF84BABF4CF9F735C0ABD24CF0D48/Event-Subprocess-Bpmn" rel="noopener noreferrer"&gt;Image source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Issue is that it's two graphs - execution and data, which makes for a disjointed view. Could we create one compelling graph, when data has non-trivial cross-references, while also participating in different executions? As a bonus, how to represent metaprogramming - if execution also changes?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cjs3awbupc5x6w2yf4w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7cjs3awbupc5x6w2yf4w.png" alt=" " width="800" height="246"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://en.wikipedia.org/wiki/Call_graph" rel="noopener noreferrer"&gt;Image source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Data consists of entities referencing each other. But all references aren't equal. Some carry ownership information, others don't - like direct fields in a struct vs pointers. Some allow for mutation, others are read-only. Some allow for concurrent access, others have mutual exclusion.&lt;/p&gt;

&lt;p&gt;With modern programming practices, the following optimal design emerges. We want to avoid shared mutable data. Ownership gives responsibility of managing the lifecyle, and memory. For mutation by non-owner - borrowing needs to take place, to ensure that a reference stays valid for the duration of the mutation, kind of like in a database transaction. With this, we can represent data as squares, and the data that they own - as squares within. Data that they simply reference - as squares without, connected with arrows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44v1lby9x0rxjf2eaqlz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44v1lby9x0rxjf2eaqlz.png" alt=" " width="736" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, Foo owns Bar, and Bar references Baz without ownership.&lt;/p&gt;

&lt;p&gt;Now for the call graph - methods belong to objects, making them squares within squares. Methods call each other - this is represented by arrows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fek27ii8zc1qeel8p2u9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fek27ii8zc1qeel8p2u9d.png" alt=" " width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So how do we unite the two? By establishing that first there are methods, and data belongs to methods. But what about data belonging to different methods at different times? Who said our graph needs to be static - data can move as the system progresses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgyq7avubxaw39wy51wx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgyq7avubxaw39wy51wx3.png" alt=" " width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the data never needs to be in two executions at the same time - it's either copied or borrowed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa18hnatrp74zeggd6hee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa18hnatrp74zeggd6hee.png" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So this solves it! But wait. This is a pretty nice way of representing a snapshot of a live system - but what about actually programming a system? How do we represent a system for that goal?&lt;/p&gt;

&lt;p&gt;What's important to realise is that programming as we know it, isn't actually programming - it's meta-programming.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdp5l5aa5i0ydqv2zkg8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdp5l5aa5i0ydqv2zkg8q.png" alt=" " width="800" height="731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine a live system. It has instructions - when a new order is placed, update and display the sum of the cost. When a user creates an order, that changes the state of the system - how is this not programming?&lt;/p&gt;

&lt;p&gt;Now imagine a program. It has instructions - when a system is started, register a repository for orders, a web form for creating them, and a processing pipeline, which responds to changes in the repository, and calculates the sum. It's a program over the live system - it is a meta-program.&lt;/p&gt;

&lt;p&gt;Now imagine a meta-program. It has instructions - when a web form for creating orders is registered, register copies of it for the user, an internal copy for an admin, and a demo copy for the sales department. This is a program over the meta program - making it meta^2.&lt;/p&gt;

&lt;p&gt;So in reality, you're always modifying a graph - just, it's usually not the live system graph, but a graph above it, with instructions of how to modify the underlying graph. This meta-stack can be as high as you want - and the workflow for inspecing and modifying any level of those graphs is the same. And there is no code, which is "dead", and merely a template for constructing a system - all graphs are live graphs, and you are viewing their live state, as it is.&lt;/p&gt;

&lt;p&gt;Hopefully you found information in this post to be insightful, and it gave you a broader perspective on what programming is all about. I believe graphs to be the future, and if you agree or disagree - let me know what you think, by writing a comment. Thanks for reading.&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>Linux from the developer's perspective. Part 3 - Loading and running</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Sat, 12 Jul 2025 10:53:12 +0000</pubDate>
      <link>https://forem.com/lostghost/linux-from-the-developers-perspective-part-3-loading-and-running-4bmh</link>
      <guid>https://forem.com/lostghost/linux-from-the-developers-perspective-part-3-loading-and-running-4bmh</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We compiled ourselves a binary. How does it get loaded and run? And what does it do while running? Let's start with an example, in the previous post we ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ ./main 
Hello!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What exactly happens when we run &lt;code&gt;./main&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;An OS process gets created and started. A process being an independent execution, that has it's own context and resources, such as file descriptors and memory.&lt;/p&gt;

&lt;p&gt;Firstly, it is important to understand, that a process is never created for no reason - it is always started by another process. In this case, the chain of who started whom goes &lt;code&gt;./main&lt;/code&gt;&amp;lt;-&lt;code&gt;bash&lt;/code&gt;&amp;lt;-&lt;code&gt;xfce4-terminal&lt;/code&gt;&amp;lt;-&lt;code&gt;xfce4&lt;/code&gt;&amp;lt;-&lt;code&gt;X11&lt;/code&gt;&amp;lt;-&lt;code&gt;sx&lt;/code&gt;&amp;lt;-&lt;code&gt;bash&lt;/code&gt;&amp;lt;-&lt;code&gt;login&lt;/code&gt;&amp;lt;-&lt;code&gt;systemd&lt;/code&gt;&amp;lt;-&lt;code&gt;init (in initramfs)&lt;/code&gt;&amp;lt;-&lt;code&gt;systemd-boot&lt;/code&gt;&amp;lt;-&lt;code&gt;UEFI Firmware&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, a process is never started from "nothing" - it can only start within an already running process, with the &lt;a href="https://man7.org/linux/man-pages/man2/execve.2.html" rel="noopener noreferrer"&gt;exec&lt;/a&gt; system call. But you can't have more than one process this way - this is resolved by the &lt;a href="https://man7.org/linux/man-pages/man2/clone.2.html" rel="noopener noreferrer"&gt;clone&lt;/a&gt; syscall.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clone&lt;/code&gt; copies the process - and all of it's memory. But not quite - a lot of the address space of the process is shared libraries, which don't need to be copied. And those mappings that do need to be copied, for example, program stack and heap - are initially copied with Copy-on-Write, &lt;a href="https://en.wikipedia.org/wiki/Copy-on-write" rel="noopener noreferrer"&gt;CoW&lt;/a&gt;. This way, only minimal physical memory is needed for the running program, and if the process is to later be replaced by another, with &lt;code&gt;exec&lt;/code&gt; - no extra effort was wasted. &lt;/p&gt;

&lt;p&gt;So, when launching a program - first &lt;code&gt;clone&lt;/code&gt; is performed, then &lt;code&gt;exec&lt;/code&gt;. &lt;code&gt;exec&lt;/code&gt; removes the current process memory map, mapping the segments of the target ELF executable process image instead. After that, a jump to ENTRY symbol (usually &lt;code&gt;_start&lt;/code&gt;) is performed.&lt;/p&gt;

&lt;p&gt;Speaking of mappings - they are performed by the &lt;a href="https://man7.org/linux/man-pages/man2/mmap.2.html" rel="noopener noreferrer"&gt;mmap&lt;/a&gt; syscall (or the internal kernel version of it, when the kernel makes mappings implicitly, during the execution of &lt;code&gt;exec&lt;/code&gt;). While usually programs create anonymous mappings for the heap, or they are automatically created by hitting the guard page for the stack, this time it's an actual file mapping - and both portions of the file being mapped, and the memory they are mapped to, need to align at page boundaries. If the offset of a segment within a file is not aligned, the segment is mapped with a portion of the segment next to it, if it doesn't fill a page exactly - it is zero-extended.&lt;/p&gt;

&lt;p&gt;This is how a statically-linked program is launched. What about a dynamically linked one? It requires an interpreter to be launched first. So, for our &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ readelf -a ./main
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64

...

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000310 0x0000000000000310  R      0x8
  INTERP         0x00000000000003b4 0x00000000000003b4 0x00000000000003b4
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the last line - this means, that the program &lt;code&gt;/lib64/ld-linux-x86-64.so.2&lt;/code&gt; is launched instead of our program. Typically this is a dynamic loader - but it can be any program, really!&lt;/p&gt;

&lt;p&gt;The linker that runs at compile time assigns addresses wherever they are needed, exports symbols, and creates relocations. Relocations are needed because libraries should be loadable at any address - so calls to functions in them should be indirect. So what you get is a &lt;a href="https://en.wikipedia.org/wiki/Global_Offset_Table" rel="noopener noreferrer"&gt;GOT&lt;/a&gt; - a table that gets modified at runtime, which points to the actual locations of the functions. Think of it this way - a library exports a table of function addresses, but not as direct addresses - because it doesn't know where the library will be loaded - but as offsets from the "base" address instead. And when the library does get loaded, and the "base" address is established - you only need to add offsets to that base address, to know where in memory the functions are stored. This is a simplification, but I feel it is a helpful one.&lt;/p&gt;

&lt;p&gt;So, when a dynamically linked program is started, the dynamic linker/loader is started first, which looks at the headers of the executable, and finds the list of dynamic libraries that are needed. Then it looks for those libraries - the rules for where to look are listed in &lt;code&gt;/etc/ld.so.conf&lt;/code&gt;. There is a cache for library locations - &lt;code&gt;/etc/ld.so.cache&lt;/code&gt;. And finally, the path where to look can be overriden entirely, with &lt;a href="https://en.wikipedia.org/wiki/Rpath" rel="noopener noreferrer"&gt;rpath&lt;/a&gt;. Those libraries themselves may depend on other libraries - the search is recursive.&lt;/p&gt;

&lt;p&gt;There is one more thing to keep in mind with dynamic libraries - versioning. By convention, libraries have &lt;a href="https://unix.stackexchange.com/a/293782" rel="noopener noreferrer"&gt;3 version numbers&lt;/a&gt; - breaking ABI release, backwards-compatible ABI release, internal change. So the file on disk is &lt;code&gt;libfoo.so.X.Y.Z&lt;/code&gt;. But the program using the library doesn't care about Y and Z - it just needs a compatible X version. So in the program header, it requests &lt;code&gt;libfoo.so.X&lt;/code&gt; but then - how does a dynamic linker/loader match the requested &lt;code&gt;libfoo.so.X&lt;/code&gt; against the actually existing &lt;code&gt;libfoo.so.X.Y.Z&lt;/code&gt;? By the use of symlinks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libfoo.so -&amp;gt; libfoo.so.X -&amp;gt; libfoo.so.X.Y -&amp;gt; libfoo.so.X.Y.Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These symlinks are created by &lt;code&gt;ldconfig&lt;/code&gt;. Typically it is run after any system upgrade.&lt;/p&gt;

&lt;p&gt;Why do we need the symlinks, why not just have &lt;code&gt;libfoo.so&lt;/code&gt; on disk? Well, if some programs need different X versions of the same library. In practice, the package manager tracks all of these version numbers, so you never have that situation - but this provides a mechanism for if you do. Ok then, why not have &lt;code&gt;libfoo.so.X&lt;/code&gt; on disk, and not the Y and Z? That's for if you are upgrading a system, you can do that atomically - put the new file alongside the old one, switch the symlink, delete the old file. There's never a point when the library is missing. Of course, now that the industry uses immutable images, this is largely redundant.&lt;/p&gt;

&lt;p&gt;And after all that, the dynamic linker &lt;code&gt;mmap&lt;/code&gt;s the libraries and our executable into memory, performs relocations, resolves symbols and writes them into the GOT (or delayes resolving them - in which case the &lt;a href="https://maskray.me/blog/2021-09-19-all-about-procedure-linkage-table" rel="noopener noreferrer"&gt;PLT&lt;/a&gt; is created. In short - it is a special "default" place to jump for unresolved functions, which resolves the function, writes the address into the GOT, and jumps there - so on subsequent calls the GOT is used right away.&lt;/p&gt;

&lt;p&gt;Do we actually need all of this machinery? I'd argue we don't - just having static binaries, that &lt;code&gt;dlopen&lt;/code&gt; loadable modules is a much simpler mechanism. But it's still important to know how dynamic libraries work, even if purely for being a well-rounded professional.&lt;/p&gt;

&lt;p&gt;This is it for now - in the next blog, we will cover debugging a program. So stay tuned for that!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>linux</category>
    </item>
    <item>
      <title>Linux from the developer's perspective. Part 2 - Compilation and linking</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Tue, 01 Jul 2025 18:52:40 +0000</pubDate>
      <link>https://forem.com/lostghost/linux-from-the-developers-perspective-part-2-compilation-and-linking-5na</link>
      <guid>https://forem.com/lostghost/linux-from-the-developers-perspective-part-2-compilation-and-linking-5na</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;series&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How does a C program get compiled? For C-like languages, compilation involves four steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preprocessing, compile-time metaprogramming&lt;/li&gt;
&lt;li&gt;Compilation itself, translation of the source code to assembly&lt;/li&gt;
&lt;li&gt;Assembling, turning assembly into machine code in an object file&lt;/li&gt;
&lt;li&gt;Linking, turning the object file into an executable or a library&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, all these categories, except for linking, are to some degree arbitrary. Preprocessing is an anomaly, a language within a language, a crutch - it exposes the limited expressive power of the base language. Compilation is to a degree arbitrary, because you can embed assembly code into C code, which doesn't require compilation. Assembly is not actually assembly - it's Gnu Assembly, the universal assembly. Originally, the assembly language was described in the &lt;a href="https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24593.pdf" rel="noopener noreferrer"&gt;ISA Manual&lt;/a&gt;, and the manufacturer provided with it the assembler itself, which read and compiled the assembly - GNU Assembly is not that. It's a higher-level, universal assembler. Still, the mental framework of these four steps is a net positive, but past a point of experience, you can see gaps in the structure.&lt;/p&gt;

&lt;p&gt;We already discussed the preprocessor in the previous blog, let's now turn our attention to compilation. Compile our test program like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ gcc -S main.c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or rather, for more clean, unoptimized assembly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ gcc -S -O0 -fno-asynchronous-unwind-tables -fno-unwind-tables -fno-ident -fno-stack-protector main.c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resulting assembly with explanatory comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    .file   "main.c"
    .text
    .globl  main
    .type   main, @function
main:
    pushq   %rbp                  # Prologue: save old base pointer
    movq    %rsp, %rbp            # Set new base pointer
    subq    $16, %rsp             # Allocate 16 bytes for local variables

    movl    %edi, -4(%rbp)        # Save argc (1st argument, int) at -4(%rbp)
    movq    %rsi, -16(%rbp)       # Save argv (2nd argument, char **) at -16(%rbp)

    cmpl    $1, -4(%rbp)          # Compare argc to 1
    jg      .L2                   # If argc &amp;gt; 1, jump to .L2 (print argument)

    movl    $1, %eax              # argc &amp;lt;= 1: set return value to 1
    jmp     .L3                   # Return

.L2:
    movq    -16(%rbp), %rax       # Load argv into %rax
    addq    $8, %rax              # Advance to argv[1] (first argument, skipping program name)
    movq    (%rax), %rax          # Dereference: load pointer to argument string
    movq    %rax, %rdi            # Move that pointer to %rdi (argument for puts)
    call    puts@PLT              # Print argv[1] with puts()
    movl    $0, %eax              # Set return value to 0

.L3:
    leave                         # Epilogue: restore frame pointer and stack
    ret                           # Return to caller

    .size   main, .-main
    .section    .note.GNU-stack,"",@progbits

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, many C constructs translate into assembly directly. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;int a = 10, b = 20, c;
c = a + b;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translates to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mov eax, 10
mov ebx, 20
add eax, ebx
mov c, eax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;int arr[4] = {1, 2, 3, 4};
int *p = &amp;amp;arr[2];
*p = 99;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translates to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mov eax, [arr + 8]   ; access arr[2] (int, 4 bytes each)
mov dword ptr [arr + 8], 99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in a way, C is just higher-level assembly. But in other ways, it isn't - some constructs don't have a translation, producing undefined behavior. Structs, enums and unions are higher-level datatypes, which don't have a direct assembly counterpart. Calling conventions vary between CPUs and OS'es. In fact, if you want to explore, how exactly does code translate into assembly - there is a really useful website for that, &lt;a href="https://godbolt.org/z/qGo6z8azq" rel="noopener noreferrer"&gt;GodBolt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After compilation comes assembly, which translates assembly code into machine code, for a given ISA. But it doesn't output just text - it outputs a binary image. Specifically, one in an &lt;a href="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format" rel="noopener noreferrer"&gt;ELF&lt;/a&gt; format.&lt;/p&gt;

&lt;p&gt;But the resulting artifact is an object file, which isn't the final process image. It contains information about sections (.text, .data, .bss) and their contents (machine code, using section-relative addresses), as well as references to symbols imported from external libraries. However, machine code uses section-relative addresses - addresses based on offsets from start of sections. But because we don't yet know at which address these sections are loaded - so we can't run the program yet. What lays out the sections in memory, thus turning them into segments, is a linker - and it does so with a linker script. On Arch Linux, these are at &lt;code&gt;/lib/ldscripts/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's examine one. Take &lt;code&gt;elf_x86_64.x&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64") // self-explanatory
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start) // which symbol is the entry point to the executable
SEARCH_DIR("/usr/x86_64-pc-linux-gnu/lib64"); SEARCH_DIR("/usr/lib"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/usr/x86_64-pc-linux-gnu/lib"); // which directories to look for for libraries, while linking
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000));
  . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
  /* Place the build-id as close to the ELF headers as possible.  This
     maximises the chance the build-id will be present in core files,
     which GDB can then use to locate the associated debuginfo file.  */
  .note.gnu.build-id  : { *(.note.gnu.build-id) }
  .interp         : { *(.interp) }
  .hash           : { *(.hash) }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows the mapping of sections into segments, starting at address &lt;code&gt;0x400000&lt;/code&gt;.&lt;br&gt;
Let's now link the program manually&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ gcc -c main.c 
[lostghost1@archlinux c]$ ld main.o --dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/crt1.o -lc -o main
[lostghost1@archlinux c]$ ./main hello
hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When invoking &lt;code&gt;ld&lt;/code&gt;, our linker, we needed to specify the path to the dynamic loader (which is specified as &lt;code&gt;--dynamic-linker&lt;/code&gt; - quite confusing), because we are compiling a dynamic and not a static executable - more on the distinction later. crt1.o is a special object file, part of the standard C library, which contains the entry point (the &lt;code&gt;_start&lt;/code&gt;) symbol. &lt;code&gt;-lc&lt;/code&gt; is libc, &lt;a href="https://www.sourceware.org/glibc/" rel="noopener noreferrer"&gt;glibc&lt;/a&gt; in our case - alternatives such as &lt;a href="https://musl.libc.org/" rel="noopener noreferrer"&gt;musl&lt;/a&gt; libc exist.&lt;/p&gt;

&lt;p&gt;Now let's inspect the binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ readelf -a main
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          13088 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         12
  Size of section headers:           64 (bytes)
  Number of section headers:         24
  Section header string table index: 23

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         00000000004002e0  000002e0
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .hash             HASH             0000000000400300  00000300
       0000000000000018  0000000000000004   A       4     0     8
  [ 3] .gnu.hash         GNU_HASH         0000000000400318  00000318
       000000000000001c  0000000000000000   A       4     0     8
  [ 4] .dynsym           DYNSYM           0000000000400338  00000338
       0000000000000048  0000000000000018   A       5     1     8
  [ 5] .dynstr           STRTAB           0000000000400380  00000380
       0000000000000039  0000000000000000   A       0     0     1
  [ 6] .gnu.version      VERSYM           00000000004003ba  000003ba
       0000000000000006  0000000000000002   A       4     0     2
  [ 7] .gnu.version_r    VERNEED          00000000004003c0  000003c0
       0000000000000030  0000000000000000   A       5     1     8
  [ 8] .rela.dyn         RELA             00000000004003f0  000003f0
       0000000000000018  0000000000000018   A       4     0     8
  [ 9] .rela.plt         RELA             0000000000400408  00000408
       0000000000000018  0000000000000018  AI       4    18     8
  [10] .plt              PROGBITS         0000000000401000  00001000
       0000000000000020  0000000000000010  AX       0     0     16
  [11] .text             PROGBITS         0000000000401020  00001020
       0000000000000075  0000000000000000  AX       0     0     16
  [12] .rodata           PROGBITS         0000000000402000  00002000
       0000000000000004  0000000000000004  AM       0     0     4
  [13] .eh_frame         PROGBITS         0000000000402008  00002008
       0000000000000088  0000000000000000   A       0     0     8
  [14] .note.gnu.pr[...] NOTE             0000000000402090  00002090
       0000000000000040  0000000000000000   A       0     0     8
  [15] .note.ABI-tag     NOTE             00000000004020d0  000020d0
       0000000000000020  0000000000000000   A       0     0     4
  [16] .dynamic          DYNAMIC          0000000000403e60  00002e60
       0000000000000180  0000000000000010  WA       5     0     8
  [17] .got              PROGBITS         0000000000403fe0  00002fe0
       0000000000000008  0000000000000008  WA       0     0     8
  [18] .got.plt          PROGBITS         0000000000403fe8  00002fe8
       0000000000000020  0000000000000008  WA       0     0     8
  [19] .data             PROGBITS         0000000000404008  00003008
       0000000000000004  0000000000000000  WA       0     0     1
  [20] .comment          PROGBITS         0000000000000000  0000300c
       000000000000001b  0000000000000001  MS       0     0     1
  [21] .symtab           SYMTAB           0000000000000000  00003028
       0000000000000180  0000000000000018          22     5     8
  [22] .strtab           STRTAB           0000000000000000  000031a8
       00000000000000a6  0000000000000000           0     0     1
  [23] .shstrtab         STRTAB           0000000000000000  0000324e
       00000000000000cc  0000000000000000           0     0     1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We see that we still have the section headers - along with the program headers! Let's remove all of it, since we won't be debugging this executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ strip --strip-section-headers main
[lostghost1@archlinux c]$ readelf -a main
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         12
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0

There are no sections in this file.

There are no section groups in this file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better!&lt;/p&gt;

&lt;p&gt;Now on the difference between static and dynamic executables. Object files that call out to external functions, produce unresolved symbols. They are resolved during linking - when the executable is laid out in program segments, the points where functions are called get replaced with jumps to the actual function addresses. This makes for a static executable. However, we can choose to postpone resolving the symbols - and resolve them at program start. Then, we will declare which libraries we need, and which symbols from them are needed - and at program start, the linker will run first, find those libraries, load them, and resolve the symbols. This makes for a dynamic executable.&lt;/p&gt;

&lt;p&gt;Let's see which one our program is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ ldd main
    linux-vdso.so.1 (0x00007ffedcd23000)
    libc.so.6 =&amp;gt; /usr/lib/libc.so.6 (0x0000756dbada8000)
    /lib64/ld-linux-x86-64.so.2 =&amp;gt; /usr/lib64/ld-linux-x86-64.so.2 (0x0000756dbafc0000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both libc and the loader are needed at runtime (&lt;code&gt;linux-vdso&lt;/code&gt; is a special pseudo-library). That makes the executable dynamic.&lt;/p&gt;

&lt;p&gt;Glibc &lt;a href="https://stackoverflow.com/questions/57476533/why-is-statically-linking-glibc-discouraged" rel="noopener noreferrer"&gt;shouldn't&lt;/a&gt; produce static executables. To compile one, install musl-libc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ yay -S musl clang
[lostghost1@archlinux c]$ musl-clang --static main.c -o main
[lostghost1@archlinux c]$ ldd main
    not a dynamic executable
[lostghost1@archlinux c]$ ./main hello
hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This executable has all its symbols resolved - no dynamic loader needed!&lt;/p&gt;

&lt;p&gt;Lastly, let's touch upon compiling dynamic and static libraries themselves. A static library is just an archived object file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ cat main.c
#include &amp;lt;stdio.h&amp;gt;
#include "sayhello.h"
int main(int argc, char** argv){
    sayhello();
    return 0;
}
[lostghost1@archlinux c]$ cat sayhello.h
#ifndef _SAYHELLO_H
#define _SAYHELLO_H
void sayhello();
#endif
[lostghost1@archlinux c]$ cat sayhello.c
#include &amp;lt;stdio.h&amp;gt;
void sayhello(){
    printf("Hello!\n");
}
[lostghost1@archlinux c]$ musl-clang -c sayhello.c
[lostghost1@archlinux c]$ musl-clang -c main.c
[lostghost1@archlinux c]$ ar q libsayhello.a sayhello.o
ar: creating libsayhello.a
[lostghost1@archlinux c]$ musl-clang --static main.o -L. -lsayhello -o main
[lostghost1@archlinux c]$ ldd main
    not a dynamic executable
[lostghost1@archlinux c]$ ./main 
Hello!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;-L.&lt;/code&gt; means "look in this directory", &lt;code&gt;-lsayhello&lt;/code&gt; means "look for a file libsayhello.a" (&lt;code&gt;.a&lt;/code&gt; because we specified &lt;code&gt;--static&lt;/code&gt;, otherwise it would be &lt;code&gt;.so&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;As for a dynamic library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ rm main
[lostghost1@archlinux c]$ gcc -shared sayhello.o -o libsayhello.so
[lostghost1@archlinux c]$ gcc main.o -L. -lsayhello -o main
[lostghost1@archlinux c]$ ldd main
    linux-vdso.so.1 (0x00007ffc384aa000)
    libsayhello.so =&amp;gt; not found
    libc.so.6 =&amp;gt; /usr/lib/libc.so.6 (0x000074e040a48000)
    /lib64/ld-linux-x86-64.so.2 =&amp;gt; /usr/lib64/ld-linux-x86-64.so.2 (0x000074e040c65000)
[lostghost1@archlinux c]$ ./main 
./main: error while loading shared libraries: libsayhello.so: cannot open shared object file: No such file or directory
[lostghost1@archlinux c]$ LD_LIBRARY_PATH=. ./main 
Hello!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typically we don't look in the current directory - neither for executables (which is why we have to specify &lt;code&gt;./&lt;/code&gt; when running &lt;code&gt;./main&lt;/code&gt;), nor for libraries - this is for security reasons, so that we don't accidentally run what we didn't intend to. Which is why we have to resort to specifying the environment variable.&lt;/p&gt;

&lt;p&gt;Of course, the shared library advertises it's exported symbol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ readelf -a libsayhello.so
...
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND [...]@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (2)
     6: 0000000000001110    20 FUNC    GLOBAL DEFAULT   11 sayhello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all I have to share, when it comes to compiling and linking a C program. In the next blog we will examine loading and running an ELF executable file. See ya then!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>linux</category>
    </item>
    <item>
      <title>Linux from the developer's perspective. Part 1 - C language introduction</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Sun, 29 Jun 2025 09:57:38 +0000</pubDate>
      <link>https://forem.com/lostghost/linux-from-the-developers-perspective-part-1-c-language-introduction-21lb</link>
      <guid>https://forem.com/lostghost/linux-from-the-developers-perspective-part-1-c-language-introduction-21lb</guid>
      <description>&lt;p&gt;This blog is part of a &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;series&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unix is the programmer's OS. Let's see that for ourselves. A further exploration of the material shown here can be found in this &lt;a href="https://en.wikipedia.org/wiki/Advanced_Programming_in_the_Unix_Environment" rel="noopener noreferrer"&gt;book&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Linux and C share a lot of their DNA - the syscall API is a C API. Understanding Linux helps you understand C, and vice versa. To get an overview of how the whole mechanism functions, let's compile and run a simple C program, and carefully examine the process.&lt;/p&gt;

&lt;p&gt;We will develop in the terminal, on a minimal Linux installation. How to get such an installation - refer to an earlier &lt;a href="https://dev.to/lostghost/linux-from-the-users-perspective-part1-installing-linux-2gea"&gt;blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The minimal program will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ cat main.c 
#include &amp;lt;stdio.h&amp;gt;
int main(int argc, char** argv){
    if (argc&amp;lt;2) return 1;
    printf("%s\n",argv[1]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I suggest using the &lt;code&gt;nano&lt;/code&gt; editor. Let's go over it line-by-line.&lt;/p&gt;

&lt;p&gt;On line 1 there is a preprocessor directive - those start with &lt;code&gt;#&lt;/code&gt;. A preprocessor is a special kind of language, programs in which run at compile time. It's used for metaprogramming, modifying the way the program behaves. Modern languages try to avoid creating a second language for metaprogramming, but that's not the case with C.&lt;/p&gt;

&lt;p&gt;In C, the compilation unit is a file - the compiler considers one file at a time. So if the file needs function signatures, to be able to call into libraries - the signatures need to be included into every file. This is what the &lt;code&gt;#include&lt;/code&gt; directive does - it takes the file that you pass to it, and copies the entire contents to the place where the &lt;code&gt;#include&lt;/code&gt; was written. We can see that for ourselves, by running the C PreProcessor, &lt;code&gt;cpp&lt;/code&gt; (that requires installing the package &lt;code&gt;gcc&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ cpp main.c
# 0 "main.c"
# 0 "&amp;lt;built-in&amp;gt;"
# 0 "&amp;lt;command-line&amp;gt;"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "&amp;lt;command-line&amp;gt;" 2
# 1 "main.c"
...
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
# 949 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 973 "/usr/include/stdio.h" 3 4

# 2 "main.c" 2

# 2 "main.c"
int main(int argc, char **argv){
    if (argc&amp;lt;2) return 1;
    printf("%s\n",argv[1]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see a lot of text, and in the end - our program. The mass of text that precedes the program, is all that was included from our &lt;code&gt;#include&lt;/code&gt; directive - plus metadata, such as files from which the included text came from. Of course, &lt;code&gt;#include&lt;/code&gt; is a pretty crude way of being able to call libraries - modern languages use module systems, instead of header files.&lt;/p&gt;

&lt;p&gt;What does a header file look like? You could find the &lt;code&gt;stdio.h&lt;/code&gt; header file - it should be at &lt;code&gt;/usr/include/stdio.h&lt;/code&gt;. Or we can write our own header file, for our program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ cat main.h
#ifndef _MAIN_H
int main(int argc, char **argv);
#define _MAIN_H
#endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And include it into our program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#include "main.h"
#include &amp;lt;stdio.h&amp;gt;
int main(int argc, char **argv){
    if (argc&amp;lt;2) return 1;
    printf("%s\n",argv[1]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Include with &lt;code&gt;&amp;lt;these&amp;gt;&lt;/code&gt; braces means including from the system directory (&lt;code&gt;/usr/include/&lt;/code&gt;), while with &lt;code&gt;"these"&lt;/code&gt; means from the custom one - including the current directory.&lt;/p&gt;

&lt;p&gt;In the header file, there are weird preprocessor directives, above and below the actual signature. That is called an include guard - it prevents the same header from being included twice. Which, is another argument in favor of a proper module system - you don't need hacks like this one.&lt;/p&gt;

&lt;p&gt;Next comes the function header - it's our main function, from which the program starts running (more on that later). It returns an &lt;code&gt;int&lt;/code&gt;, and takes two arguments - &lt;code&gt;argc&lt;/code&gt;, and an array of strings &lt;code&gt;argv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's first discuss the return value. It's only a loose convention, of which values mean what. If the function performs an operation - typically the return value of zero means success, and a positive number - means an error code. If it returns an address - then the value of 0, which is &lt;code&gt;NULL&lt;/code&gt;, means failure, and a positive number means a valid address. Sometimes numbers from 0 and up are all valid - like with file descriptors, returned by the &lt;code&gt;open&lt;/code&gt; syscall, in which case the value &lt;code&gt;-1&lt;/code&gt; means an error. When it comes to program exit status, which is what the &lt;code&gt;main&lt;/code&gt; function returns - despite the data type being a signed &lt;code&gt;int&lt;/code&gt;, only values 0-255 are allowed - where 0 means success, and any other number is an error code. Error codes aren't standard across programs either - you need to look at the documentation for a particular program, to find out what it means. Of course, a better system would be a standard set of exception attributes - about if the exception is fatal, if it's transient, which module does it relate to, what is the cause. Some exceptions would be more specific, others - less. But for now, it's just numbers.&lt;/p&gt;

&lt;p&gt;Next let's take a look at datatypes. They in large part correspond to the machine data types, that the processor operates on - C is not far removed from assembly. There's signed and unsigned numbers, a character, which is actually a single byte, bool, float, struct, union and pointer. Pointer is special - it is actually not a "real" datatype - it is just an unsigned number, wide enough to store an address for the current architecture. You can easily convert a pointer to a number and back. A pointer to 0, a NULL pointer, is a special kind of pointer, which is technically valid - you can dereference it - but typically you don't want to dereference it, because you probably didn't intend to look at the value at address 0, and it is a programming mistake if you do.&lt;/p&gt;

&lt;p&gt;Next is another datatype, that doesn't really exist - an array. An array is just a pointer, and to index into the array you use an offset. So these are equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*(argv+1)
argv[1]
1[argv]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How then do you determine where the array ends? You use a separate variable. Or, you use a decades-old dirty trick.&lt;/p&gt;

&lt;p&gt;Another datatype, that doesn't really exist - a character string. A character string is just an array, which is just a pointer. But there is no second variable to determine the length of the string. Instead, there is an implicit contract - the last character of a string will be a zero byte, a NULL. So, if you print out a string - the printing function will start at the start address, and continue reading byte-by-byte - until it finds the NULL character. If for some reason it's not there - the function will continue reading either up to an unmapped page boundary, in which case there will be a &lt;a href="https://en.wikipedia.org/wiki/Segmentation_fault" rel="noopener noreferrer"&gt;SegFault&lt;/a&gt;, or a random other NULL byte - and if between the start and the NULL byte, there was stored, for example, a password - it will be printed out as well. As you can imagine, historically, this has been a major source of vulnerabilities.&lt;/p&gt;

&lt;p&gt;When writing a program, you are doing computer maths. And you would prefer to operate with actual mathematical concepts, and not concrete computer ones - you shouldn't care about how many bits are in a number, or if an array is a pointer and an offset. That's not the role C plays - it's close to hardware. For a higher-level system, look into how integers are handled in Python - when small, they use the native datatype. But if they get too large - they implicitly convert to "BigInteger" implementation, and don't just loop around.&lt;/p&gt;

&lt;p&gt;So then, what is &lt;code&gt;char**&lt;/code&gt;? It's an array of NULL-terminated character strings, the length of which is passed in the &lt;code&gt;argc&lt;/code&gt; variable. The array contains command-line arguments, with which the program was launched - the first argument always contains the path to the executable itself.&lt;/p&gt;

&lt;p&gt;So then, what does the program do? It prints out it's first actual command-line argument. Compile it and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lostghost1@archlinux c]$ gcc main.c
[lostghost1@archlinux c]$ ./a.out hello
hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success! But wait, what did this &lt;code&gt;gcc&lt;/code&gt; program actually do? And how exactly was the resulting executable, well, executed? That's for a later blog. See ya then!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>linux</category>
    </item>
    <item>
      <title>Linux from the user's perspective - Part 3: Graphical Interface</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Wed, 25 Jun 2025 16:51:37 +0000</pubDate>
      <link>https://forem.com/lostghost/linux-from-the-users-perspective-part-3-graphical-interface-kd8</link>
      <guid>https://forem.com/lostghost/linux-from-the-users-perspective-part-3-graphical-interface-kd8</guid>
      <description>&lt;p&gt;This blog is part of a series. More &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before reading this blog, I recommend watching this &lt;a href="https://youtu.be/cj02_UeUnGQ" rel="noopener noreferrer"&gt;talk&lt;/a&gt;. I am very much influenced by it, and I don't want to rip it off.&lt;/p&gt;

&lt;p&gt;Graphics are pretty cool, right? Along with having multiple applications running, that you can interact with at the same time. But all applications need to be able to interact with the graphics system in a consistent way - which requires a common standard. Let's see what the industry came up with.&lt;/p&gt;

&lt;p&gt;The X11 protocol, and it's open-source implementation X.Org have a rich history. Originally designed for a network-transparent mainframe-centric model, along with the vast zoo of incompatible displays, keyboards, mice, and the like, they had to be repurposed for the modern age. Over the course of that journey, it went through three stages of evolution, and how has a successor - Wayland.&lt;/p&gt;

&lt;p&gt;I won't touch on history of the protocol's development - the talk linked above does an excellent job already. Let's discuss the technical side. In the original incarnation, the vision was as follows.&lt;/p&gt;

&lt;p&gt;You have a powerful mainframe, and a graphics-capable terminal. First you run the X server on the terminal - it takes control of the screen, the keyboard and the mouse. It loads from the disk fonts, bitmaps, themes and styles, configs. It does so by the virtue of the &lt;code&gt;startx&lt;/code&gt; script that runs the commands from &lt;code&gt;~/.xinitrc&lt;/code&gt;, which typically had the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xrdb -merge ~/.Xresources &amp;amp;    # load x resources
xsetroot -solid grey &amp;amp;         # background color
xclock -geometry 50x50+40+40 &amp;amp; # simple app
xterm -geometry 80x50+20+150 &amp;amp; # terminal
exec twm                       # window manager; 'exec' makes it keep X running until WM exits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this is only a part of the configuration. Configuration of devices goes into &lt;code&gt;xorg.conf&lt;/code&gt; or &lt;code&gt;xorg.conf.d/&lt;/code&gt; file/directory, located at /etc/X11/xorg.conf.d/, /usr/share/X11/xorg.conf.d/, ~/.xorg.conf.d/ - they are merged together.&lt;/p&gt;

&lt;p&gt;Then the X server is started - and it would query for available XDMCP remotes. XDMCP daemon would run on a remote host, a mainframe, and connect to the X server via TCP. The X server would present a login screen. After login, a remote desktop environment would be started, and you could start apps - all of them would run remotely, on the mainframe.&lt;/p&gt;

&lt;p&gt;When a graphical app would start, it would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set up a TCP socket to the X server, via Xlib&lt;/li&gt;
&lt;li&gt;use themes, fonts, bitmaps, that were loaded by the X server&lt;/li&gt;
&lt;li&gt;get a window, which is a framebuffer to be rendered-to in immediate mode&lt;/li&gt;
&lt;li&gt;send drawing commands - put pixel, draw line, draw square, draw text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As for arranging windows on a screen, a special privileged client connected to the X server - the window manager. Just as regular apps would subscribe to events, including keyboard events, the window manager would subscribe to window events - window created, destroyed, moved, resized. It would then tell the X server, where to place windows. This allowed for clean separation of concerns, and allowed the X server to stay flexible, and allow for different window management strategies, by adhering to the "mechanism, not policy" philosophy.&lt;/p&gt;

&lt;p&gt;Let's configure a server, for exactly that experience. We will use the VM as the XDMCP server, so the X server should already be running on your host. Make sure it allows for TCP connection, by editing &lt;code&gt;/etc/X11/xinit/xserverrc&lt;/code&gt;. Also authorize the remote server with the command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xhost +&amp;lt;IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  Configuring the network and the unprivileged user
  &lt;br&gt;
To prepare the host, let's configure the network with NetworkManager, and disable dhcpcd.&lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[root@archlinux ~]# pacman -S networkmanager
[root@archlinux ~]# systemctl enable networkmanager
[root@archlinux ~]# nmtui # configure the interfaces here
[root@archlinux ~]# systemctl disable dhcpcd@enp1s0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let's now create an unprivileged user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[root@archlinux ~]# useradd -m -G adm,wheel,tty,sys -s /usr/bin/bash user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allow root without a password&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[root@archlinux ~]# pacman -S sudo
[root@archlinux ~]# nano /etc/sudoers # input the text below
root ALL=(ALL) ALL
Defaults targetpw
ALL ALL=(ALL) NOPASSWD: ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up password, switch to the user&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[root@archlinux ~]# passwd user
[root@archlinux ~]# sudo su user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Now install the xfce4 desktop environment, xorg, and gdm - it will be our XDMCP daemon&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user@archlinux ~]$ sudo pacman -S xfce4 xorg gdm
[user@archlinux ~]$ sudo systemctl enable gdm
[user@archlinux ~]$ sudo systemctl start gdm
[user@archlinux ~]$ nano ~/.xsession # set contents to
startxfce4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;/etc/gdm/custom.conf&lt;/code&gt;, set contents to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[daemon]
WaylandEnable=false

[security]
DisallowTCP=false

[xdmcp]
Enable=true

[chooser]

[debug]
Enable=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now, on your local host, run from TTY:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user@archlinux ~]$ sudo X :2 -query &amp;lt;ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, from an emulated terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user@archlinux ~]$ Xephyr :2 -query 192.168.100.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66r6f49noz5b6516q3ib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F66r6f49noz5b6516q3ib.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fow5et8nyn1515wsdsj8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fow5et8nyn1515wsdsj8b.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(this is actually running Gnome, idk, whatever)&lt;/p&gt;

&lt;p&gt;And while this currently works (even if over plain TCP - use SSH or TLS tunneling), the modern approach is remote desktop - RDP or VNC. The difference of what's being transmitted is vector graphics vs raster graphics. And X11 does vector graphics poorly, at least from what I've surmised. I'd love to see a modern immediate-mode vector graphics rendering technology that works remotely, but alas, we are stuck with raster graphics for now. Speaking of remote desktop, let's set it up!&lt;/p&gt;

&lt;p&gt;First, install &lt;a href="https://github.com/Jguer/yay" rel="noopener noreferrer"&gt;yay&lt;/a&gt;&lt;br&gt;
Then run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user@archlinux ~]$ yay -S xrdp
[user@archlinux ~]$ nano ~/.xsession # set to text below
xfce4-session
[user@archlinux ~]$ nano /etc/X11/Wrapper.config # set to text below
allowed_users=anybody
needs_root_rights=no
[user@archlinux ~]$ sudo systemctl start xrdp.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now connect to it, for example, with &lt;a href="https://wiki.archlinux.org/title/Remmina" rel="noopener noreferrer"&gt;Remmina&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvj64zohi9b0bqd3vubi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvj64zohi9b0bqd3vubi.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9ee6ad824prlsx8by4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9ee6ad824prlsx8by4k.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is with xrdp - modern desktop enviroments, such as KDE and GNOME provide their own built-in RDP servers, which you can use.&lt;/p&gt;

&lt;p&gt;Since we are discussing RDP, it would be useful to know, how it actually works under the hood. On X, any client can read the contents of the entire window, with XGetImage - and send them over. With a modern alternative, like Wayland, there is a special protocol - you can only read the contents of the windows that the user authorizes you to read.&lt;/p&gt;

&lt;p&gt;The next step of evolution came with supporting hardware acceleration, through shared memory and graphics APIs - like OpenGL, with the xgl extension. Of course - none of this is possible over a network. So, local X was the new paradigm.&lt;/p&gt;

&lt;p&gt;We can also forward any single application. Simply run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user@archlinux ~]$ export DISPLAY=&amp;lt;hostIP&amp;gt;:0
[user@archlinux ~]$ xterm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct display number can be seen in &lt;code&gt;env&lt;/code&gt; on the host&lt;br&gt;
Result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xwqp4s1mx3jfhbeh029.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xwqp4s1mx3jfhbeh029.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final step of evolution came with compositors, which broke an important architectural decision in the X server. Namely - because every application drew to its window in immediate mode, you couldn't have some windows cast shadows on other windows, or have them be transparent. That would require baking such interactions into the protocol, and making every application support them - which would be impossible. So, a hack was found - applications would draw not to the screen, but to an off-screen buffer. After a buffer was drawn, the compositor, which is the more powerful window manager, would be notified - it would then create it's own buffer, into which the buffers of all active applications would be overlayed in correct order, and with effects. Then only this buffer would be given to X for rendering.&lt;/p&gt;

&lt;p&gt;The trajectory of the X11's development is as follows - it performed less and less functions. More and more drawing was done either by the app, or the compositor X was just passing buffers back and forth. So, a more modern protocol was developed - Wayland. The goal of it is to be minimal, and allow apps and the compositor to do their job, and stay out of their way otherwise. It does raster graphics, and only on the current machine. Remote solutions exist, such as &lt;a href="https://github.com/neonkore/waypipe" rel="noopener noreferrer"&gt;waypipe&lt;/a&gt; - but it sends over raster graphics for a window, not vector graphics.&lt;/p&gt;

&lt;p&gt;Let's do some reflection. Which is better - raster graphics or vector graphics? I say vector graphics, but you have to do them well, and they are more complicated. Next would be running applications remotely - how do you do that well? Ideally, you would run applications on a cluster - and not care about which individual server that is. But that requires that all resources an application may use are also available on the cluster - which is not a trivial problem. For now, seems that local apps + RDP is what the industry settled for.&lt;/p&gt;

&lt;p&gt;That's it, when it comes to the Graphical Interface on Linux - next time, we will discuss system administration.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Linux from the user's perspective - Part 2: Terminal Interface</title>
      <dc:creator>lostghost</dc:creator>
      <pubDate>Thu, 19 Jun 2025 08:15:44 +0000</pubDate>
      <link>https://forem.com/lostghost/linux-from-the-users-perspective-part-2-terminal-interface-3k5i</link>
      <guid>https://forem.com/lostghost/linux-from-the-users-perspective-part-2-terminal-interface-3k5i</guid>
      <description>&lt;p&gt;This blog is the second in a series. More &lt;a href="https://dev.to/lostghost/linux-deep-dive-introduction-5b8c"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Linux traces its lineage to the era of computations, where the command line was the primary means of interacting with the computer. Nowadays we use the keyboard to input text, but otherwise our primary means of interaction with programs is through pointing to and activating interactive elements on the screen - with a touchscreen, a touchpad, a mouse. A further streamlining of that process would be eye-tracking, or a direct link to the brain.&lt;/p&gt;

&lt;p&gt;With that, you have two options for a UI - Command Line Interface - CLI and Terminal User Interface - TUI. A CLI allows you to input individual commands, and get their response printed out, wereas the TUI presents you with interactive visual elements placed around the screen - like a graphical interface, just with text instead. An example of CLI would be our main shell - bash, while a TUI would be the text editor we used - nano.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjx8wx0srv2nenl1w7d2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjx8wx0srv2nenl1w7d2q.png" alt="CLI example" width="800" height="560"&gt;&lt;/a&gt; CLI example - &lt;a href="https://kasheloff.ru/photos/bash-script-command/6" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4of21u0czhnkl3u1ptq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4of21u0czhnkl3u1ptq.png" alt="Image description" width="620" height="506"&gt;&lt;/a&gt; TUI example - &lt;a href="https://obatherbal.top/set-static-centos-nmtui-mand-work-setting.html" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The TUI offers a much richer way of interacting with the system, and yet the default UI for Linux is CLI - for two reasons. Firstly, legacy - line-oriented terminals came prior to those with a screen you could draw anywhere on. Secondly, CLI is a protocol is much easier - you just read text and write text. TUI as the main UI would require programs to agree to a bigger protocol - and the Linux ecosystem has a poor record when it comes to agreeing on protocols.&lt;/p&gt;

&lt;p&gt;Protocols are difficult - this will be a recurring theme. Both CLI and TUI use the terminal. Now, how hard is it to agree on how to input and output text? Harder than it first appears. The terminal requires a control protocol - what kind of mode to switch to, where on the screen to place the cursor, which sound effect to play, and a data protocol - which character to output. Historically, control information is passed in-band, on the data channel - in the form of special ASCII characters, and escape sequences. For example, character number 7 means to ring a bell. And the sequence &lt;code&gt;\033[H&lt;/code&gt; means "move the cursor to the top left corner of screen". More information can be found &lt;a href="https://wiki.osdev.org/Terminals" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Problem is that historically, these sequences were not standardized. Which is why libraries such as &lt;a href="https://en.wikipedia.org/wiki/Termcap" rel="noopener noreferrer"&gt;termcap&lt;/a&gt; were resorted to. At least the character encodings were standardised - you had Baudot code, ITA-2, EBCDIC, ASCII.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzyh3z05sqjh6t193nawd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzyh3z05sqjh6t193nawd.png" alt="Image description" width="800" height="649"&gt;&lt;/a&gt;&lt;br&gt;
Ascii table - &lt;a href="https://en.wikipedia.org/wiki/File:ASCII-Table.svg" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Linux emulates &lt;a href="https://en.wikipedia.org/wiki/VT100#Variants" rel="noopener noreferrer"&gt;VT102&lt;/a&gt;/&lt;a href="https://en.wikipedia.org/wiki/VT220" rel="noopener noreferrer"&gt;VT220&lt;/a&gt; terminals, with the help of the &lt;a href="https://en.wikipedia.org/wiki/Getty_(software)" rel="noopener noreferrer"&gt;getty&lt;/a&gt; program.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrlt59s48rarmj0wfqsf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrlt59s48rarmj0wfqsf.png" alt="Image description" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
VT100 - &lt;a href="https://www.oreilly.com/radar/bots-in-the-enterprise/" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the current technologies and standards, how would we implement a terminal? It's job is to take text input, and print out text at the correct screen position. But nowadays, our monitors are graphical, our physical connectors such as HDMI, DisplayPort, ThunderBolt are graphical - we don't need to send text over a wire, we can send the rendered graphics. Then we don't need special control characters in the encoding. To render text into graphics, the OS kernel would load the font, the application would configure the size of the terminal, and that's it - the application can input and output text. For cross-platform rendering of graphics, there is a UEFI standard - &lt;a href="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html?highlight=graphics%20output#console-i-o-protocol" rel="noopener noreferrer"&gt;Console I/O Protocol&lt;/a&gt; &lt;a href="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html?highlight=graphics%20output#simple-text-output-protocol" rel="noopener noreferrer"&gt;Text Output Protocol&lt;/a&gt;, &lt;a href="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html?highlight=graphics%20output#simple-text-input-protocol" rel="noopener noreferrer"&gt;Text Input Protocol&lt;/a&gt;, &lt;a href="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html?highlight=graphics%20output#graphics-output-protocol" rel="noopener noreferrer"&gt;Graphics Output Protocol&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Over the raw terminal, runs a special program that allows you to input not just text, but commands - that program being a shell. Based on user commands, the shell interacts with the kernel - and through it, possibly with other programs. The original shell for Unix was the &lt;a href="https://en.wikipedia.org/wiki/Thompson_shell" rel="noopener noreferrer"&gt;Thompson Shell&lt;/a&gt;, while the one most commonly used nowadays is the &lt;a href="https://en.wikipedia.org/wiki/Bash_(Unix_shell)" rel="noopener noreferrer"&gt;Bourne Again Shell&lt;/a&gt;. There are other shells available, such as the slimmed-down &lt;a href="https://en.wikipedia.org/wiki/Almquist_shell#Dash" rel="noopener noreferrer"&gt;dash&lt;/a&gt;, the expanded &lt;a href="https://en.wikipedia.org/wiki/Z_shell" rel="noopener noreferrer"&gt;zsh&lt;/a&gt;, the unique &lt;a href="https://en.wikipedia.org/wiki/Fish_(Unix_shell)" rel="noopener noreferrer"&gt;fish&lt;/a&gt;. But for myself and many others, returns are diminishing after bash.&lt;/p&gt;

&lt;p&gt;These shells support aliasing long commands by short names, embedding subcommands into a larger command, and doing general programming, with commands, variables and functions, conditions and loops, making them Turing-complete. As with any Turing-complete system, there is a point at which programming in it is optimal. Some Linux users push that boundary, when it comes to shell scripting.&lt;/p&gt;

&lt;p&gt;A shell allows you to perform administrative tasks, or launch an application. To launch an application, just input it's name! Well, not so simple. The shell carries state, that will influence how the application will be run, and whether it will be executed successfully. The shell state consists of, among other things, the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current directory&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Internal variables&lt;/li&gt;
&lt;li&gt;umask setting&lt;/li&gt;
&lt;li&gt;User that is logged in&lt;/li&gt;
&lt;li&gt;ulimits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them influence how and if an application is run. I would argue that only the current logged in user should influence the application, but that's not how it is today.&lt;/p&gt;

&lt;p&gt;The current application can be interrupted with &lt;code&gt;Ctrl+C&lt;/code&gt;, shut down with &lt;code&gt;Ctrl+\&lt;/code&gt;, suspended with &lt;code&gt;Ctrl+Z&lt;/code&gt;, resumed with &lt;code&gt;fg&lt;/code&gt;, or run in the background with &lt;code&gt;bg&lt;/code&gt;. But what if you want multiple active programs in one terminal? For that you would need a terminal multiplexer, a middleground between a CLI and a TUI - there are options such as GNU screen and tmux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7cf6os0e3blmiyvazie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7cf6os0e3blmiyvazie.png" alt="Image description" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
tmux - &lt;a href="https://unix.stackexchange.com/questions/219296/how-to-go-mouseless?rq=1" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Within the application, you would want to point to things - for example, to move the cursor to the desired position in text. Previously, terminals didn't have mouse support - that required sophisticated keyboard shortcuts to be able to point the cursor to the desired position. For example, in programs such as &lt;a href="https://en.wikipedia.org/wiki/Vi_(text_editor)" rel="noopener noreferrer"&gt;vi&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/GNU_Emacs" rel="noopener noreferrer"&gt;emacs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fun1wfiypsnmsi5henmda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fun1wfiypsnmsi5henmda.png" alt="Image description" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
Emacs - &lt;a href="https://www.xataka.com/aplicaciones/viviendo-vida-casi-interfaz-grafica-asi-trabajan-linuxeros-que-ven-todo-dentro-consola-modo-texto-1" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But enough yapping - let's actually do something. One of the most common task on a computer is editing a document - so let's edit one in the console, like a person from the 80's would do. We will use some modern tools, but the feeling will be the same.&lt;/p&gt;

&lt;p&gt;Start up the VM, remount the filesystem read-write, configure the network&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo 'nameserver 8.8.8.8' &amp;gt; /etc/resolv.conf
mount -o remount,rw /
pacman -S nano
systemctl enable dhcpcd@enp1s0 # - replace with the correct interface
systemctl start dhcpcd@enp1s0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can do the remount automatically, by editing the "/etc/fstab" file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo '/ / none remount,rw 0 0' &amp;gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's set up telnet, for easier copy-pasting of commands&lt;br&gt;
First, add the IP address for the host network, from within the VM - you can see the appropriate subnet, if you check the addresses for the second virtual interface (first one being used for NAT - correlate the one on the host with the one on the VM) on the host. For me that was:&lt;/p&gt;

&lt;p&gt;On the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4: virbr0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc htb state UP group default qlen 1000
    link/ether 52:54:00:d3:77:04 brd ff:ff:ff:ff:ff:ff
    inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
       valid_lft forever preferred_lft forever
5: virbr1: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc htb state UP group default qlen 1000
    link/ether 52:54:00:21:87:cf brd ff:ff:ff:ff:ff:ff
    inet 192.168.100.1/24 brd 192.168.100.255 scope global virbr1
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2: enp1s0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:5d:53:69 brd ff:ff:ff:ff:ff:ff
    altname enx5254005d5369
    inet 192.168.122.241/24 brd 192.168.122.255 scope global dynamic noprefixroute enp1s0
       valid_lft 2630sec preferred_lft 2180sec
    inet6 fe80::865a:a008:b8a:d06b/64 scope link 
       valid_lft forever preferred_lft forever
&amp;lt;enp2s0 was empty - not shown&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First one is NAT, so second one (enp2s0) will be the LAN. Thus, on the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip a a 192.168.0.2/24 dev enp2s0
ip link set up enp2s0
systemctl start telnet.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permit root login via telnet - add the line "pts/0" to /etc/securetty.&lt;br&gt;
And login from the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;telnet 192.168.100.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's install the needed packages for the actual demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pacman -Sy vim texlive-basic texlive-latex texlive-doc texlive-mathscience wget
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And download the example document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget https://github.com/mundimark/markdown-vs-latex/raw/refs/heads/master/samples/sample2e.tex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open it for editing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim sample2e.tex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj2ip12ts8s8av164680.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj2ip12ts8s8av164680.png" alt="Image description" width="800" height="972"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's change the year from 1994 to 1995. Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:%!sed 's/1994/1995/g'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyog4bmsv5ptbrwzfkxhk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyog4bmsv5ptbrwzfkxhk.png" alt="Image description" width="277" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To save the changes, input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:w
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile to dvi format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:!latex %
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And preview the document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:!dvi2tty sample2e.dvi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3394hny1hxbhprs3dvhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3394hny1hxbhprs3dvhe.png" alt="Image description" width="800" height="817"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you would send the dvi file to the line printer.&lt;/p&gt;

&lt;p&gt;Scroll to the end with spacebar, then exit vim with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:wq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should give you an idea of how documents were edited back in the day.&lt;/p&gt;

&lt;p&gt;Now what does this say about the CLI ecosystem of end-user applications? It consists of large programs, such as vim and emacs - and small programs, that perform their individual tasks on text buffers, that can then plug into the big editors. And for scripting, the same small commands can integrate with the shell. In this way, it forms a complete, coherent, text-based echosystem. The ecosystem goes much further - hand-editable text-based config files, git as a text-oriented database, text-oriented logs, email stored as text files, the network protocols being just text over TCP (as is the case with HTTP, &lt;a href="https://youtu.be/mrGfahzt-4Q" rel="noopener noreferrer"&gt;SMTP&lt;/a&gt;, FTP), the docs in the form of man pages and texinfo files, being text - these are a few examples. Here is a &lt;a href="https://youtu.be/gd5uJ7Nlvvo" rel="noopener noreferrer"&gt;talk&lt;/a&gt; on the subject.&lt;/p&gt;

&lt;p&gt;These small programs were originally (and still largely are, but at least grouped into packages) individual executable files, but nowadays tools such as &lt;a href="https://www.busybox.net/" rel="noopener noreferrer"&gt;busybox&lt;/a&gt; and &lt;a href="https://landley.net/toybox/faq.html" rel="noopener noreferrer"&gt;toybox&lt;/a&gt; exist - they provide one binary file, which implements a lot of the commands. This makes the system better partitioned. Speaking of which, do you know the original meaning of a &lt;a href="https://oldhacker.org/txt/Phreaks_Only/busybox.txt" rel="noopener noreferrer"&gt;busybox&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;But documents don't exist in a vacuum - they are stored as files, in a filesystem. The filesystem is a lot, but from a certain angle, it's a tree-oriented database. Both system files and user documents are stored in this database - and both uses have different priorities when it comes to the database. With system state, you want the mechanism for storage, modification, and retrieval, to be specialised to the task, guarantee consistency, and hide implementation. With documents, you want arbitrary grouping, for example, with a tag-oriented database, or a knowledge graph, or both - with collaborative editing, saving of previous versions, synchronisation with other devices. The filesystem is a compromise that provides a good enough interface for both usecases. But if an OS were to be implemented from scratch in the modern times - would we actually need a filesystem?&lt;/p&gt;

&lt;p&gt;In the next blog, we will take a look at the graphical interface.&lt;/p&gt;

</description>
      <category>linux</category>
    </item>
  </channel>
</rss>
