During the discussion, an intriguing question is raised first: which came first, the operating system or the application?

This is a question I often pondered while studying computer science as an undergraduate.

The answer might seem obvious at first glance: since applications run on top of the operating system, it would naturally follow that the operating system came first. However, the answer to this question is not so simple. In fact, only by addressing this question can you truly understand the essence of computers and operating systems, and grasp why it is still important to study assembly, C, operating systems, and more today.

Eliminate Unnecessary Overthinking

Many people get stuck on the definition of "application" within this very question. They debate: Is the program on a device that functions without relying on an operating system, such as a calculator, considered an application?

According to the Oxford Dictionary's definition of "Application": Application is a computer program designed for a particular purpose. Any computer program used to solve a specific problem can be referred to as an application.

Whether it is the Apollo navigation system, the flight control computers of Airbus, the short videos on smartphones, or even the T-800 from Terminator, they are all running computer programs composed of algorithms and data structures, and they are solving specific problems. Therefore, they all qualify as fitting the definition of Application.

The Paradox Inherent in the Problem Itself?

Which came first, the operating system or the application? Which one was born first, and which one later?

Of course, many people would instinctively say: Applications run on top of the operating system, so naturally the operating system came first. Equally, others would point out: The operating system was created to run applications, so applications must have come first.

Today, if you release a new Linux distribution, it can run almost all Linux applications out of the box. Clearly, it was born after the applications. On the other hand, if you release the HarmonyOS operating system today, you would need to urgently call on developers to create software for your new system once it is ready. Clearly, it was also born after the applications.

Does this question itself lead to a paradox? Why do people keep arguing endlessly about this?

But before we delve into deeper thinking, we need to first look at the earliest computers and assembly language.

Assembly Language and Program Composition

Before diving into deeper thinking, we need to first look at assembly language.

Read the following code:

#include <stdio.h>

struct Point {
    int x;
    int y;
};

void print_point(struct Point p) {
    printf("Point(%d, %d)\n", p.x, p.y);
}

int main() {
    struct Point p = {1, 2};
    print_point(p);
    return 0;
}

When I compile this code with gcc and generate assembly code, I can see the following content:

.section .data
.LC0:
    .string "Point(%d, %d)\n"

.section .text
.globl print_point
print_point:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    movl    %eax, %edi
    movl    -8(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %eax
    movq    %rax, %rdi
    movl    $0, %eax
    call    printf
    leave
    ret

.globl main
main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    $1, -4(%rbp)
    movl    $2, -8(%rbp)
    movl    -4(%rbp), %edi
    movl    -8(%rbp), %esi
    call    print_point
    movl    $0, %eax
    leave
    ret

In this assembly code, we can clearly see the distinction between the data segment and the code segment:

  • .data segment: Used to store global variables and string constants. In this example, .LC0 is a string constant "Point(%d, %d)\n".
  • .text segment: Used to store code. In this example, it includes the implementation of the print_point function and the main function.

Keep in mind that these will ultimately become binary, neatly arranged in memory and on disk, with each line having its own address. Through this example, we can see that when the compiler converts C code into assembly code, it generates two main segments: the data segment and the code segment. The data segment contains global variables and constants needed during program execution, while the code segment contains the program's instructions. Both the data segment and the code segment have their own addresses. From the perspective of memory, they are all data.

.section .data
.LC0:
0x1000	.string "Point(%d, %d)\n"

.section .text
.globl print_point
print_point:
0x1010	pushq   %rbp
0x1014	movq    %rsp, %rbp
0x1018	subq    $16, %rsp
0x101C	movl    %edi, -4(%rbp)
0x1020	movl    %esi, -8(%rbp)
0x1024	movl    -4(%rbp), %eax
0x1028	movl    %eax, %edi
0x102C	movl    -8(%rbp), %eax
0x1030	movl    %eax, %esi
0x1034	movl    $.LC0, %eax
0x1038	movq    %rax, %rdi
0x103C	movl    $0, %eax
0x1040	call    printf
0x1044	leave
0x1048	ret

.globl main
main:
0x1050	pushq   %rbp
0x1054	movq    %rsp, %rbp
0x1058	subq    $16, %rsp
0x105C	movl    $1, -4(%rbp)
0x1060	movl    $2, -8(%rbp)
0x1064	movl    -4(%rbp), %edi
0x1068	movl    -8(%rbp), %esi
0x106C	call    print_point
0x1070	movl    $0, %eax
0x1074	leave
0x1078	ret

How to Debug Assembly

Before I continue, I’d like to share a bit about how I first started learning assembly.

Assembly language is unlike any other language we’ve studied. It deals directly with the CPU’s hardware instruction set. The translation process is so simple that it barely qualifies as “compilation.”

The concept of assembly predates operating systems. Naturally, the assembly code I wrote... I didn’t want to run it within an OS. It’s clearly manipulating the CPU; my work involved moving data back and forth between registers. Truly understanding assembly means being able to execute code directly on the CPU, rather than working through an IDE like we do with other high-level languages.

file

Of course, what if I accidentally corrupted the registers on my own computer?

Debugging assembly code using a virtual machine is an effective and safe method. We can treat the edited assembly program as a virtual disk, write it directly to the MBR (Master Boot Record), and then boot the virtual machine. It will then execute those assembly instructions.

A virtual machine provides an isolated sandbox environment. This sandbox contains a virtual CPU and registers, making debugging very straightforward.

Once the virtual machine boots, I have complete control over that chip! Every action it takes is a result of my instructions.

https://stackoverflow.com/questions/13021466/using-a-virtual-machine-to-learn-assembly

The compiled assembly code mentioned above is actually x86 assembly, which is not always the dominant standard. In the early days, different computers had their own assembly languages, such as IBM’s S/360 and DEC’s PDP-11. x86 assembly became mainstream starting with the introduction of the 8086 processor by Intel in the late 1970s, which was compatible with 8-bit and 16-bit architectures. Since then, Intel’s 80286, 80386, and Pentium series processors have maintained this compatibility, allowing software to run on newer processors. Microsoft’s MS-DOS and Windows operating systems further popularized x86 assembly. Compared to other early assembly languages, x86’s market support and compatibility advantages have established it as the mainstream standard.

Today’s compilers all translate high-level languages into low-level languages. For example, C code is compiled into assembly code, which is then compiled into machine code.

Machine code is the only set of instructions that hardware chips can execute directly.

In the 1950s, Grace Hopper developed the first compiler, A-0, which made it possible to write programs using English-like statements instead of complex machine code. The emergence of compilers greatly simplified the programming process, although the final output still had to be machine code executable by hardware. This illustrates the core function of a compiler: translating human-readable high-level languages into computer-executable machine code, thereby enabling program execution.

Keep in mind that the example above tells us that even without an operating system, we can develop and run programs, as long as we load the machine code directly into memory and let the CPU execute it.

The example above also demonstrates that even without an operating system, we can develop and run programs. Many university operating system course assignments precisely have students use assembly language, run it in a virtual machine, and observe the execution results of the assembly code.

In early computers, programmers set binary switches directly on the control panel to input machine code and then start the program. Modern microcontrollers work similarly; they do not require an operating system and can run simply by writing the compiled machine code into flash memory. While discussing all of this, the concept of an operating system has not yet been introduced.

The Birth of Operating Systems

file

https://en.wikipedia.org/wiki/PDP-7

The traditional development model mentioned above remained prevalent for a long time, until the 1960s, when people began to realize that computers required more efficient management and operation methods. At that time, many researchers and engineers had to write specific control code for each program to run particular applications. In 1969, Ken Thompson wanted to run the "Space Travel" game on a PDP-7 computer, so he manually created a small utility to manage memory to run his little game.

Early computers, such as the IBM 704, had a very painful user experience because the machine had to be reconfigured every time a new program was run. To improve efficiency, scientists developed batch processing systems, which enabled automatic loading and sequential execution of programs. However, competition for access to these critical research computing resources remained intense. Laboratories had to schedule engineers in a queue to use the computers. Many low-priority, long-running tasks wasted significant hardware resources.

As time-sharing and virtual memory technologies gradually emerged, multiple programs were allowed to share hardware resources and run independently, avoiding interference with one another. Through process isolation and memory management, the concept of the operating system became increasingly clear, becoming the core of modern computer systems, ensuring resource allocation and security in multi-tasking environments.

file

https://en.wikipedia.org/wiki/IBM_704#/media/File:IBM_Electronic_Data_Processing_Machine_-_GPN-2000-001881.jpg

In research at the Massachusetts Institute of Technology, CTSS (Compatible Time-Sharing System) was a significant milestone. CTSS was the first true time-sharing operating system, allowing multiple users to work on a single computer simultaneously. The development of this system further advanced the evolution of multitasking operating systems, laying the foundation for modern operating systems. It was only at this point that the concept of operating systems began to become clearly defined and emerged as an important field of study in computer science.

One of the fundamental concepts of operating systems is the process. A process is an instance of a program in execution, containing the program's code as well as current state information, such as register contents and data in memory. Process management is one of the key responsibilities of an operating system, ensuring that multiple processes can run efficiently and securely on the same system.

Although operating systems play a crucial role in process management, it is important to note that when a process is actually executed, instructions are still executed directly by the CPU. The operating system does not translate or intervene between individual instructions; rather, it manages the execution order and resource allocation of processes through scheduling mechanisms. A process is essentially a program, consisting of machine code targeted at the CPU. The operating system is not responsible for actually computing the execution results of a process; it only manages CPU time and the lifecycle of processes.

For example, consider the multiple applications running on a computer in daily use. Suppose you have a web browser, a game, and a music player open simultaneously; these three applications correspond to three distinct processes. The operating system uses process scheduling to determine which process occupies the CPU time slice at any given moment, thereby enabling multitasking. Even if your game battle is intense, the music player can continue to play smoothly, thanks to the operating system's process management and scheduling. From the user's perspective, the operating system creates a seamless experience that makes it seem as if some programs are "running in the background." In reality, all programs run directly on the CPU, and even "background" programs occupy CPU time slices at specific moments.

Reviewing the C code above

Looking back at the C code above, we can further explore function pointers and their memory operations at runtime. A pointer to a function is not merely a data type; it actually represents an execution point during program execution. This execution point can be dynamically specified and invoked, a flexibility that has profound implications for the relationship between operating systems and applications.

First, let's look at a simple example of how to use a pointer to a function:

#include <stdio.h>

void hello() {
    printf("Hello, World!\n");
}

int main() {
    void (*func_ptr)();
    func_ptr = hello;
    printf("Address of function hello: %p\n", hello);
    printf("Address stored in func_ptr: %p\n", func_ptr);
    func_ptr();
    return 0;
}

file

In this example, we define a function hello and declare a function pointer func_ptr in the main function. We then point func_ptr to the hello function and call hello through func_ptr. This demonstrates how a function pointer can point to a specific function within a code segment and how that function can be invoked via the pointer.

Next, we can further explore how to copy an executable binary program into memory and run it. Suppose we have a binary program file program.bin, which might have been created as follows:

gcc -c -o program.o program.c
objcopy -O binary program.o program.bin

We can implement this operation with the following code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void load_and_execute(const char *filename) {
    FILE *file = fopen(filename, "rb");
    if (!file) {
        perror("Failed to open file");
        exit(EXIT_FAILURE);
    }

    // 获取文件大小
    fseek(file, 0, SEEK_END);
    long filesize = ftell(file);
    fseek(file, 0, SEEK_SET);

    // 分配内存并读取文件内容
    char *buffer = malloc(filesize);
    if (!buffer) {
        perror("Failed to allocate memory");
        fclose(file);
        exit(EXIT_FAILURE);
    }
    fread(buffer, 1, filesize, file);
    fclose(file);

    // 定义一个函数指针并指向加载到内存中的代码段
    void (*func_ptr)() = (void (*)())buffer;

    // 执行加载的程序
    func_ptr();

    // 释放内存
    free(buffer);
}

int main() {
    load_and_execute("program.bin");
    return 0;
}

In this example, we define a function load_and_execute that takes a filename as an argument. First, it opens the specified binary file and retrieves its size. Then, it allocates sufficient memory to store the file contents and reads the file contents into memory. Next, we define a function pointer func_ptr and point it to the code segment loaded in memory. Finally, we execute the loaded program by calling func_ptr.

Alternatively, you can directly execute the following code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

unsigned char add_bin[] = {
    // 这里是针对 x86_64 架构的 add 函数的二进制机器码
    0x55,                   // push   %rbp
    0x48, 0x89, 0xe5,       // mov    %rsp,%rbp
    0x89, 0x7d, 0xfc,       // mov    %edi,-0x4(%rbp)
    0x89, 0x75, 0xf8,       // mov    %esi,-0x8(%rbp)
    0x8b, 0x55, 0xfc,       // mov    -0x4(%rbp),%edx
    0x8b, 0x45, 0xf8,       // mov    -0x8(%rbp),%eax
    0x01, 0xd0,             // add    %edx,%eax
    0x5d,                   // pop    %rbp
    0xc3                    // retq
};

void load_and_execute() {
    // 获取页面大小
    size_t pagesize = sysconf(_SC_PAGESIZE);

    // 分配可读、可写、可执行的内存页
    void *exec_mem = mmap(NULL, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC,
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (exec_mem == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    // 将函数的机器码复制到可执行内存中
    memcpy(exec_mem, add_bin, sizeof(add_bin));

    // 定义一个函数指针并指向加载到内存中的代码段
    int (*add_func)(int, int) = (int (*)(int, int))exec_mem;

    // 调用加载的函数
    int result = add_func(2, 3);
    printf("Result of add(2, 3): %d\n", result);

    // 释放内存
    if (munmap(exec_mem, pagesize) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }
}

int main() {
    load_and_execute();
    return 0;
}

In the example above, I wrote a piece of binary machine code directly in C code, used mmap to copy it into memory, pointed a pointer to it, and let the CPU execute it directly. Finally, I used munmap to release the memory. Surprisingly, it actually runs and produces a result.

file

This example demonstrates how to load and execute a binary program without an operating system. In the previous example, I took a shortcut by placing the machine code directly in a char[]. It would be more convincing if this content were read from a file. At that point, the file could be considered an "executable file."

In fact, early computer systems executed programs directly in this manner. This also explains why understanding function pointers and low-level memory operations is so crucial for grasping the essence of computers and operating systems.

Once we deeply understand these low-level operations, we can better comprehend the design and implementation of operating systems. By managing and scheduling various programs in memory, operating systems enable multitasking and resource management. These fundamental operations, such as loading and executing binary programs, are among the core mechanisms of how operating systems function.

Phased Summary

Which came first: operating systems or applications?

Strictly speaking, chips and instruction sets came first, followed by applications, and then operating systems. Chips and instruction sets form the foundation of computing, defining the basic operations a computer can perform.

Early computers like ENIAC performed calculations by directly manipulating hardware, which is equivalent to running the most primitive applications. These applications were very simple sequences of instructions, relying entirely on hardware with no intermediate layer. Later, as applications became increasingly complex, operating systems emerged.

So, is the answer to this question really that applications came first, and then operating systems were developed to solve certain problems? As applications evolved and new requirements arose, operating systems continued to address additional issues. Is this a spiral of continuous development?

Hmmmm

In our upcoming study, we will need to revisit some undergraduate-level knowledge:

Dependency Inversion Principle

The Dependency Inversion Principle is one of the five object-oriented design principles mentioned by Robert C. Martin in his classic book Agile Software Development: Principles, Patterns, and Practices. This principle emphasizes that high-level modules should not depend on low-level modules; both should depend on abstractions. Specifically, dependencies between modules should be managed through abstractions (interfaces or abstract classes) wherever possible, rather than through concrete implementations.

Let's illustrate this principle with a practical example. Suppose we want to implement a simple light and switch system. In a traditional design, the switch class directly depends on the light class:

class Light {
    public void turnOn() {
        System.out.println("The light is on");
    }

    public void turnOff() {
        System.out.println("The light is off");
    }
}

class Switch {
    private Light light;

    public Switch(Light light) {
        this.light = light;
    }

    public void operate(String command) {
        if (command.equals("ON")) {
            light.turnOn();
        } else if (command.equals("OFF")) {
            light.turnOff();
        }
    }
}

In this design, the Switch class directly depends on the Light class, which violates the Dependency Inversion Principle. According to the Dependency Inversion Principle, we should depend on abstractions rather than concrete implementations. We can define a Switchable interface to represent any device that can be turned on and off:

interface Switchable {
    void turnOn();
    void turnOff();
}

class Light implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("The light is on");
    }

    @Override
    public void turnOff() {
        System.out.println("The light is off");
    }
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate(String command) {
        if (command.equals("ON")) {
            device.turnOn();
        } else if (command.equals("OFF")) {
            device.turnOff();
        }
    }
}

Now, the Switch class depends on the Switchable interface rather than the concrete Light class. This makes it easier to extend the system, such as adding a new Fan class, without modifying the Switch class:

class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("The fan is on");
    }

    @Override
    public void turnOff() {
        System.out.println("The fan is off");
    }
}

Similar principles apply in other domains as well. For example, when learning to drive at a driving school, we learn to operate a motor vehicle, not a specific brand like Tesla, Mercedes-Benz, or Cadillac. We are acquiring an abstract skill, which allows us to drive any brand of car.

file

Another example is in software development. If I need to use Redis, I am not truly dependent on Redis itself, but rather on a key-value (KV) database. Redis is one implementation of this, and we can equally use other KV databases, as long as they conform to the same abstract interface.

There are many other applications of this principle. For instance, when learning programming, we are not truly learning Python, Java, or C++ themselves, but rather a way of expressing a series of computations that a computer can perform. These programming languages are merely implementations of that expressive method.

By understanding and applying the Dependency Inversion Principle, we can build systems that are more flexible and scalable. This principle not only helps us decouple modules but also makes our systems more modular and easier to maintain. This is why understanding this principle is crucial for software developers.

We have introduced the Dependency Inversion Principle to enable extensibility in software development. Next, let’s discuss how software is extended.

Building Plugin-Enabled Applications

By combining the Dependency Inversion Principle, we can build pluggable applications with high flexibility and extensibility. The core of building pluggable applications lies in first designing abstract interfaces that define how plugins express their functionality and report their status. Once the interfaces are defined, the development of plugins and the main application can proceed in parallel, with the main application relying solely on these abstract interfaces to load and invoke plugins.

A typical example is the plugin system in games. For instance, World of Warcraft allows players to extend game functionality by writing Lua scripts. Plugin authors only need to follow the API interfaces provided by the game to implement complex features, such as custom interfaces and combat information statistics. Similarly, Minecraft has a powerful (though often cumbersome to upgrade) plugin system, where players can create new gameplay, items, and mechanics by implementing specific interfaces.

file

Another example is the city-building simulation game Cities: Skylines, which supports plugins through the Steam Workshop. Players can create and share custom buildings, vehicles, and terrain. Developers provide a set of standard interfaces for plugins, ensuring seamless integration into the game.

This pluggable design is not limited to games but is widely used in other software systems. For example, many modern browsers like Chrome and Firefox use plugin systems to allow users to extend browser functionality. Developers only need to write plugins according to the API interfaces provided by the browser, without modifying the browser's core code.

The success of a plugin system hinges on the design of abstract interfaces. A good abstract interface should possess the following characteristics:

  1. Simplicity and Clarity: The interface should be as simple as possible, making it easy to understand and implement.
  2. High Decoupling: The interface should minimize dependencies on specific implementations, maintaining low coupling between modules.
  3. Extensibility: The interface design should consider future extension needs, allowing new features to be added without breaking the existing system.

For example, suppose we are developing an image processing application and want to support various image filter plugins. We can first define an abstract interface:

public interface ImageFilter {
    void applyFilter(Image image);
}

Then, any class that implements this interface can be loaded as a plugin into the application. For example, we can create a grayscale filter plugin:

public class GrayscaleFilter implements ImageFilter {
    @Override
    public void applyFilter(Image image) {
        // 灰度滤镜的实现
    }
}

The main program dynamically loads these plugins that implement the ImageFilter interface using reflection mechanisms or a plugin manager, and then invokes their applyFilter method to process images. This way, no matter how many new filters need to be added in the future, you only need to write plugins according to this interface specification, without modifying the main program's code.

By adopting this approach, we not only enhance the system's flexibility and maintainability but also provide opportunities for third-party developers to participate and extend the system. This is a practical application of the Dependency Inversion Principle: by depending on abstract interfaces rather than concrete implementations, we can build systems that are more elastic and scalable.

Understanding ABI

Let's return to our main point: many school assembly courses require students to be able to run their own assembly code on a virtual machine.

Suppose you have built an assembly application that runs directly on the CPU, with no concept of an operating system. Your application is already quite functional, but now you need to allow third-party developers to create plugins for it. How would you design this?

At this point, you need to define an interface that describes the specifications for its plugins. This interface is called the ABI. ABI stands for Application Binary Interface. It describes how an application interacts with the operating system or other applications. It may define how a plugin starts, how it requests resources, its priority, how it releases resources, and how it terminates.

This interface is an abstraction. It can even exist before the assembly application itself is created.

It includes the program's entry point (such as WinMain on Windows and main on Linux), icon and resource file management (Windows embeds them via resource files, while Linux defines them via .desktop files), instruction set (such as x86, x86_64, ARM, etc.), dynamic link library management (Windows uses DLLs, Linux uses .so files), calling conventions (such as __stdcall on Windows and cdecl on Linux), and executable file formats (PE on Windows and ELF on Linux). These specifications ensure that different programs can run compatibly on the same system.

By defining a clear ABI, your assembly application can have plugins developed by others. At this stage, your assembly application has effectively become an operating system. You need to manage the loading and unloading of plugins, handle resource contention between them, and ensure the stability and security of the system.

Now, you have understood the essence of an operating system. An operating system is essentially an executor and manager of an ABI. It manages the allocation of hardware resources, the scheduling of processes, and the reading and writing of files.

file

For example, as shown in the image above, WSL1 actually implements the Linux ABI, allowing Linux applications to run on the Windows kernel.

The definition of ABI is the bridge between the operating system and applications. Understanding ABI means understanding the core functions of the operating system. It is not merely a resource manager but an abstraction layer that enables applications to run efficiently and stably on hardware. This makes the relationship between the operating system and applications no longer mysterious, but clear and understandable through the key concept of ABI.

Is This Issue Really Important?

Once we understand that true abstraction is the ABI (Application Binary Interface), does the question of whether the operating system or the application came first really matter? Or, rather, should there even be a concrete answer to it?

One implements the ABI, while the other utilizes it. The ABI defines the interface between software and the operating system. It provides a standard for applications, enabling them to run on different operating systems without any modifications to the application itself.

The answer to this question is actually: applications and operating systems did not come into existence in a specific sequence, but both emerged after the ABI. Before the ABI existed, there were no applications and no operating systems. Once the ABI was established, applications and operating systems could come into existence simultaneously.

Taking the HarmonyOS operating system as an example, it is compatible with the Linux ABI. This means it can run Linux applications. Linux applications do not need to be recompiled to run on HarmonyOS. This fact demonstrates that operating systems and applications are not independent entities; rather, they are closely interconnected through the ABI. The compatibility of HarmonyOS allows existing Linux applications to operate within a new operating system environment.

From this perspective, the question of whether the operating system or the application came first becomes irrelevant. What matters is how we define and implement these abstractions so that hardware resources can be utilized efficiently and securely, and so that applications can run in different environments.

Looking Back on Undergraduate Studies

file

As shown in the image above. For those unfamiliar with ABI and assembly, their understanding is limited to high-level languages. To them, the behavior of operating systems, hardware, and languages feels like magic.

I am very fortunate to have taken an assembly course during my undergraduate studies. Although I have largely forgotten the specific syntax, learning assembly today is not just about writing low-level code, but about understanding how computers operate. Assembly language reveals how programs are loaded into memory, how instructions are executed by the CPU, and how memory management and resource allocation are handled. Taking object-oriented programming as an example, the implementation of virtual functions actually relies on function pointer tables. These details are hidden in high-level languages but are fully exposed in assembly. Through assembly, we gain an understanding of function pointers and see how memory addresses are managed and used, which lays the foundation for understanding object-oriented programming and polymorphism.

Topics covered in operating systems courses, such as deadlock detection, memory paging, and process scheduling, are central to computer science. Without a deep understanding of these concepts, it is impossible to truly master the underlying mechanisms of a system. For instance, while deadlock issues are rare in high-level applications, they are a critical problem that must be addressed in operating systems and database systems. Understanding the principles of memory paging and process scheduling helps us optimize program performance, design efficient algorithms, and improve system stability and responsiveness.

In the fields of data center management and network security, a profound understanding of operating systems is equally crucial. Knowing how an operating system manages resources, enforces access controls, and handles exceptions is essential for designing secure and reliable systems that can withstand potential attacks. This is even more true for compiler development and embedded system design; only by understanding the underlying hardware and operating system mechanisms can we write efficient and stable code.

In summary, studying assembly and operating systems during undergraduate studies is not merely about mastering programming techniques, but about comprehending the fundamental principles of computer science. This knowledge is not just for passing exams or completing assignments; it is for enabling us to tackle complex problems in our future careers and to design efficient, secure, and stable systems. A lack of this foundational knowledge leaves us helpless when facing complex systems, turning the computer into an incomprehensible black box. Therefore, no matter how technology evolves, this foundational knowledge remains indispensable.

Reply

Application software merely runs on top of an operating system and relies on the features and services provided by the operating system!

In computer science education and experimentation, students and researchers often write simple operating systems and applications from scratch. For example, many university operating system courses require students to write a simple operating system and then run self-made applications on it. The initial applications may be very basic, such as a program that prints "Hello, World!". These experiments demonstrate that applications can exist and run before an operating system is completed. For instance: some high-performance computing applications and real-time systems run directly on hardware without an operating system. Developers write code that interacts directly with hardware to achieve extremely high performance or real-time responsiveness.

When a computer boots, the first thing to run is the bootloader (such as BIOS or UEFI), which loads and executes the operating system kernel. In some cases, the bootloader itself can run simple applications.

Although most modern applications indeed run on top of an operating system and rely on the features and services provided by the operating system, this does not mean that applications must be born after the operating system. There is no logical connection between the two. Both rely on abstractions. Examples such as early computers, embedded systems, custom operating systems, and modern bare-metal programming all indicate that applications can exist and run independently of an operating system. Therefore, application software does not necessarily have to depend on an operating system; the relationship between applications and operating systems is more complex and diverse.