Operating System Fundamentals: Part 3 – Software Essentials!

Operating System Fundamentals: Part 3 – Software Essentials!

How Does Software Really Work?

As software developers, we often deal with programming languages like C, C++, Python, or Java. But have you ever wondered what happens under the hood when your code runs on a CPU? In today’s post, let’s dive into the essentials of software execution and how it interacts with hardware, in particular OPCODE, compilation, and stack management. Here’s a breakdown:

The Language of CPUs: OPCODE

You’ve likely heard the phrase, “CPUs understand only 1’s and 0’s.” But it’s not just random bits floating around—these are known as OPCODE, the binary instructions that the CPU decodes to perform operations. CPUs use instruction decoders to turn OPCODE into actions like moving data, performing arithmetic, or interacting with memory.

But as programmers, we don’t write OPCODE directly. That’s where assembly language comes in, bridging the gap between human-readable commands (like MOV A, 10) and the CPU’s binary instruction set.


From OPCODE to Programming Languages

Writing assembly code is still close to the hardware level, which can be cumbersome for solving complex problems. That’s why we need higher-level languages like C. While the CPU works on OPCODE, we focus on building abstractions that make coding efficient and easier to manage.

In C, we define data types, create pointers for memory manipulation, and use structures like arrays and unions to organize data. Abstractions allow us to move from low-level operations to focusing on the real-world problem domain, whether that’s managing databases, controlling a robotic arm, or building a sensor system.

From Solution Domain to Problem Domain: A transition

We need some higher abstraction to program complex sophisticated algorithms and entity. The more we move from solution domain (i.e., Computer/CPU) to Problem domain (where the actual problem we try to solve/execute exists like sensor control system, Electronic control units, databases, traffic monitoring system etc. etc. )

So we will need a form of higher levels of abstractions. Abstraction can be as simple as a data type like int float etc. along with the operations that are defined on them. A programming language such as 'C" gives you fundamental data types and operations defined on them which is known as "abstraction" in this context.

The Compilation Process: From Code to Execution

So how does your written code transform into something the CPU can understand?

Preprocessing: Handles includes, macros, and conditional compilation.

Compilation: Converts your source code into assembly.

Assembly: Translates assembly into machine code (OPCODE).



Linking: Combines machine code into an executable file, ready for the CPU to run.

The linker combines multiple object files into a single executable Object file (an ELF file). The main difference from an assembler generated object file (from a .c or .s file) is that in this linker output object file, all symbols (function calls, variables) will be resolved along with memory layouts assigned for each section (text, data, BSS).


Linker scripts can be used to control how the memory layout is assigned during this process, specifying where sections of the object file are placed in memory.

The programmer is telling the system where to place the segments. The "system" here is an abstract term, in a modern 32 bit OS like Linux, this address regions corresponds to the VA (Virtual Address) range defined by the OS specific Linker. Linker files will be mostly custom tailored for two things:

  1. The underlying HW itself - suppose you are doing bare-metal programming on a MCU or MPU
  2. The "run-time" - Could be an OS, or like a Cygwin run-time or an emulated run time whatever. Typically when we write a simple "Hello-World" code in 'C' and build and execute the ELF, the linker file will be supplied by the compiler tool chain specific to the OS such as Windows/Linux etc.


Each step turns your code into a precise set of instructions the CPU can execute, but it doesn’t stop there.

The wonderful design of 'C' function calls - The 'C' Stacks!

Stack Frames and Function Calls

Ever wondered how your program handles function calls? It’s all about the stack. When a function is called, a stack frame is created. This frame contains the function’s local variables, the return address, and space for any function arguments.

The beauty of stack frames is their ability to isolate function calls, ensuring that each function runs independently and without data corruption from others. It also enables recursion—where a function calls itself—by maintaining separate stack frames for each call.

Consider the C code for stack frame demo:



Step 1: Prologue:

Steps done in functionA, prior to calling functionB: Caller is functionA. Callee is functionB

functionA pushes processor registers such as EAX, ECX and EDX (This step is optional and will be done only if registers EAX, ECX and EDX are to be retained across function call. It’s very much likely that these registers will get changed in callee function.)

functionA pushes input parameter values required for functionB.

functionA saves EIP (current instruction pointer) by pushing that into stack. This is the return address used by functionB to return back to functionA.

NOTE: Step 3 is done automatically by ‘call’ instruction.

Step 2: Stack Pre-procesing

These are the steps done in functionB or here the callee function before starting actual computation defined in function body

functionB at first saves current EBP to stack by pushing it to stack.

functionB sets EBP to ESP, which will be used as the new stack frame address for the function

functionB allocates storage for local variables if any

functionB allocates temporary storage in stack if required. (This step is optional and is required only if some complex computation is happening in the function that may produce intermediate results)

functionB then saves EBX, ESI and EDI registers to stack. (This step is optional and is done on a need basis. Source and destination index registers are used when string operations are involved or in cases where there is movement of bulk data)

functionB now starts the actual function execution. i.e code written inside function body, will start execution only here.


Step 3: Post Processing

These are the steps done before functionB returns back to functionA

functionB saves return value to EAX register (Intel convention)

functionB restores values of EBX, ESI, EDI from stack if they got changed in course of execution of functionB. (This step is optional. Only done on a need basis)

functionB doesn’t require local vars and temporary storage anymore. So it sets back stack pointer back to EBP.

Pops ESP contents (which is actually functionA’s EBP value) to EBP. This operation brings back functionA’s stack frame.

For returning back to functionA, functionB has to perform one last operation. functionB pops out the return address from stack and jumps to that address.

NOTE 1: Above steps 3 and 4 are done using ‘leave’ instruction of Intel x86.

NOTE 2: step 5 is done using ‘ret’ instruction

Epilogue

Steps done by functionA post return from functionB

functionA doesn’t require the parameters pushed onto stack before calling functionB. So here it can get rid of these by setting the ESP accordingly.

functionA saves the return value from EAX register

functionA restores back EAX, EBX and ECX registers if required.

This is the stack frame generated for the sequence functionA calls functionB returns to functionA.

C Runtime and Execution


The hidden "C" run-time

  1. BSS Section: Uninitialized variables are set to zero.
  2. Data Segment: Initialized variables are set up.
  3. Stack Pointer: The CPU’s stack pointer is set to manage function calls.
  4. Main(): Execution jumps to the first function, and your program begins!

From writing your first line of code to seeing your software in action, this entire journey is a blend of abstraction and execution models—allowing you to turn an idea into a functioning solution.

Stay Tuned for More!

Understanding the inner workings of how software and operating systems interact with hardware is essential for building efficient and robust applications. As we’ve seen today, from OPCODE to high-level abstractions, the journey of software execution is both fascinating and empowering.

In the upcoming articles, we’ll dive deeper into even more Operating System Fundamentals, uncovering the secrets behind memory management, multitasking, kernel operations, and much more.

Got questions or insights to share? Let’s keep the conversation going in the comments below!

See you again in the next installment! Make sure to follow and stay updated with the latest in OS architecture, programming concepts, and more.


要查看或添加评论,请登录

Deepesh Menon的更多文章

社区洞察

其他会员也浏览了