Recommended reading:
Jim Ledin: Modern Computer Architecture and Organization.
Birmingham – Mumbai: Packt, 2020.
Wikipedia, selected entries. (2025-09-28)
Computers have become an integral part of our daily lives. They power everything from smartphones to hospital systems and have shaped society to such an extent that many people simply couldn't live without the hardware and software that defines the digital world.
Despite this, the majority of people still have no idea how computers work and the role of hardware and software in powering the modern technologies we use today.
Behind the sleek* screens and intricate interfaces, computer architecture forms the fundamental components and processes that make our computers tick. (Stewart 2005)
Computer architecture (CA) is the structure of a computer system made from component parts.⇒ At the highest level, the computer can be considered as a black box,⇒ while at the lowest level as a complex network of physical components like combinational and sequential circuits and logic gates.⇒
At each level, CA describes the internal organization of a computer in an abstract way that ignores details of the implementation at the lower level. At the highest level, CA defines the capabilities of the computer (from the user's viewpoint) and its programming model (from the programmer's viewpoint).
CA is the science and art of designing computers by defining the functional behavior and organization of hardware components like the CPU, memory, storage and I/O devices etc., including how they interact. CA establishes*
– the Instruction Set Architecture (ISA);
– the Microarchitecture;
– the Hardware System Architecture (HSA);
– the Macroarchitecture.
Key Components of Computer Architecture
- Instruction Set Architecture (ISA):
ISA specifies the machine's low-level programming interface at an abstract level (i.e. in a hardware-independent way).⇒ ISA defines the set of instructions a computer's processor (CPU) can execute. It acts as an interface between the software and the hardware, specifying the registers, memory addressing modes, data formats and operations. Instead of machine codes which are CPU-dependent it uses symbolic mnemonic codes and syntax. They are fully described in assembly language which provides a rather convenient representation of machine-code programs in human-readable terms.⇒- Microarchitecture:
Microarchitecture involves the detailed design and organization of how the CPU's functional units, memory hierarchy, control structures etc. are built and interconnected to implement the ISA.⇒ This level includes details like the size of caches, the organization of the CPU's pipelines, the data flow etc.
- Logic design:
Logic design is the lower-level implementation of the high-level concepts defined in the microarchitecture, focusing on the actual circuitry required to make the microarchitecture function. Logic design creates the specific logic-gate-level circuits and blocks (such as the arithmetic logic unit etc.) that implement the functions specified by the microarchitecture.- Hardware System Architecture (HSA):
HSA covers the functional organization of the major hardware subsystems of the computer system, including the CPU, memory, storage units, input/output (I/O) devices and interfaces, etc., and how they communicate through buses and control signals. Hardware architecture design is a fundamental part of systems design focusing on the blueprint* of a computer system's physical components, how these hardware elements are interconnected, their specifications, and their interactions to meet performance, reliability, and scalability requirements. Thus HSA forms the underlying foundation for the entire functionality of the computer system. programmer-visible macroarchitecture- Macroarchitecture:
The macroarchitecture is the "visible" layer of the computer system. It reflects the user's or the programmer's view of the system. They both can interact with the computer and depend on its behavior, but they don't really need to know how it's internally constructed.
The macroarchitecture consists of two things: the user-visible macroarchitecture and the programmer-visible macroarchitecture.
- The programmer-visible macroarchitecture includes high level programming languages and tools (such as compilers, interpreters, integrated development environments (IDE), development utilities and tools, software libraries, etc.) in order to provide a consistent interface to programmers. It directly affects the programmers' ability to write effective and robust applications for the computer.
- The user-visible macroarchitecture refers to the high-level structure and design of a computer system that is visible to the user, defining the interface and behavior without the intricate details of its internal implementation. This includes the hardware and software components available to the user to do specific tasks, and the interactions of these components in a large software system (e.g. in cloud computing which allows the users to access and manage their data and services from the internet, rather than relying on the local storage of a single computer).
Why Computer Architecture Matters
- Software Compatibility:
A consistent ISA ensures that the software written for that architecture can run on different computer implementations. You can have two computers that have been constructed by different companies, in different ways, with different technologies etc. but with the same architecture.- Performance and Efficiency:
CA directly impacts a computer's basic characteristics, making it crucial for designing efficient systems. For example, it highly influences the computer's speed and power, reliability, functionality, power consumption, etc., and the software that runs on it. Another characteristic of an efficient computer system is scalability which establishes* the ability of the system to handle a growing amount of workload, by usually adding extra resources to the system to maintain performance.- Innovation and Development:
Understanding architecture helps in designing specialized hardware for specific purposes (such as machine learning, neural networks, pattern recognition, artificial intelligence (AI) etc.), and creating new computing solutions.Vocabulary:
Pronunciation symbols⇒
establish [i staeblish] = to build or bring into being sth on a stable basis (Webster 2009)
syn/rel: ground, base, be the basis forScalability establishes the ability of the system to handle a growing amount of workload.
blueprint [blu:print] = a design plan or other technical drawing (e.g. a system diagram, a data flow diagram⇒ etc.)
contour [kontuö(r)] = the outline of a figure or body; the edge or line that defines or bounds a shape or object (Webster 2009)
sleek [sli:k] = having a smooth attractive shape (Longman 2009); finely contoured; streamlined (Webster 2009)
a sleek computer screen
References:
AI about "computer architecture" Google Search. (2025-09-11)
Stewart, Ellis 2025. What is Computer Architecture? Definition, Types, Structure.
https://em360tech.com/tech-articles/what-computer-architecture-definition-types-structure (2025-09-11)Illingworth, Valerie – Pyle, Ian 1996-1997. A Dictionary of Computing. Oxford – New York etc.: Oxford University Press.
Ledin, Jim 2020. Modern Computer Architecture and Organization. Birmingham – Mumbai: Packt.
Stallings, William 2018. Operating Systems. Internals and Design Principles. Edinburgh: Pearson, 2018.
Wikipedia entries: Computer Architecture etc.
Brief overview of computer system hardware (cf. Stallings 2018: 30-32)
In general, a computer consists of a processor, a main memory, and several input-output (I/O) components.
![]()
Block diagram of a computer with uniprocessor CPU
(black lines indicate the flow of control signals, whereas red lines indicate the flow of processor instructions, address information and data. Arrows indicate the direction of flow)
![]()
Single system bus architecture
- The processor or central processing unit (CPU) controls the operation of the computer and performs a few data processing functions. To achieve these purposes, the CPU has a few cooperating components (e.g. some special-purpose and general-purpose registers, the control unit, the arithmetic and logic unit etc.).
Its main purpose is to execute the machine-level instructions of programs.
- Each processor has a dedicated instruction set which contains all the machine-level instructions that a given processor can execute.
- The processor contains some registers for its operation. For example,
- the program counter (PC) or instruction pointer (IP) specifies (holds, "points to") the address of the next instruction of the currently running program to be executed;
- the instruction register (IR) holds the instruction of the currently running program to be decoded and executed.
- The processor has a special unit called execution unit (EU) which is responsible to execute arithmetic and logic operations. It contains, among others, an arithmetic-logic unit (ALU), a floating-point unit (FPU), some general-purpose registers (e.g. the accumulator, AC or AX) and other, special-purpose registers (e.g. the basic pointer, BP; the stack pointer, SP; etc.). For examle,
- Finally, the control unit (CU) of the processor manages (or directs, controls) the overall operation of the processor. It is responsible for
- controlling the flow of instructions (the instruction cycle or fetch-decode-execute(-interrupt) cycle
; note that, although this third stage is essential for the operation system, we will not deal with interrupts now),
- reading or fetching the next instruction from the memory into the instruction register (IR) according to the content of the instruction pointer (IP),
- increasing (or updating) the instruction pointer by adding the length of the current instruction to its content,
- decoding and interpreting the current instruction, and
- executing the interpreted instruction,
- controlling the execution of the instruction by sending control signals to the other units or components of the CPU.
- The main memory stores data and programs, or more precisely, those instructions which make up the currently running programs. The main memory is typically volatile, that is, when the computer is shut down, the contents of the memory are lost. (In contrast with the contents of non-volatile memories which are retained, that is, permanently stored, even when the computer is shut down.)
- A memory module consists of a set of memory cells or memory locations, defined by sequentially numbered physical addresses. Each address refers to a location or a group of locations that contain a sequence of bits that can be interpreted as either a machine-level instruction or a certain type of data.
- The processor and the main memory form the central unit of the computer.
- The main function of input-output (I/O) modules is to send, receive, store, display, print etc. data moved, for the most part, between the central unit of the computer and an I/O device. The I/O modules include a great variety of devices, e.g. a monitor, a keyboard, secondary memory devices (e.g. disks), network equipments etc.
- An I/O device usually exchanges data between the central unit of the computer and an (internal or external) buffer memory which temporarily stores data until they can be transferred and processed. A buffer is normally used to accommodate the difference in the rate at which the communicating devices can handle data during the transfer.
- The system bus is responsible for communication among processors, main memory, and I/O modules transferring data, (physical) addresses and control signals.
- In personal computer environment, the system bus is part of the motherboard which also contains the processor, the main memory, and other components (e.g. an interrupt controller, a BIOS or UEFI chip, network adapters, extension slots and cards, USB ports etc.).
The figure above illustrates the logic of the operation of the system bus. The CPU contains some (internal) registers to support data exchange among the CPU, the main memory and the I/O module. These registers and their function are as follows:
- the memory address register (MAR) contains the (physical) address of a specific location in memory for the next read or write operation
- the memory buffer register (MBR) or the memory data register (MDR) contains the data to be written into memory, or receives the data to be read from memory for the next read or write operation
- the I/O address register (I/O AR) specifies the address of a particular I/O device for the next read or write operation
- the I/O buffer register (I/O BR) refers to the data to be written into the I/O device or to be read from the I/O device during the next read or write operation
Execution of instructions (cf. Stallings 2018: 32-35)
A program to be executed by a processor consists of a sequence of machine-level instructions stored in the memory. In its simplest form, the processing of instructions consists of two basic steps:
– first, the processor reads (or fetches) the instructions from the memory one at a time, and
– second, the processor executes each instruction.
The execution of a program is a repeating process (a cycle or loop) of these two steps: the instruction fetch and the instruction execution. (Note that instruction execution may involve several operations and depends on the nature of the instruction.)The figure below illustrates the instruction cycle:
At the beginning of each instruction cycle, the processor fetches an instruction from memory. In this respect, the program counter (PC) register is of utmost importance: the PC holds the address of the next instruction to be fetched. After the instruction has been fetched, the processor increments the value of the PC so that it will hold the address of the next instruction in the sequence of instructions (i.e. in the program which is currently being executed).
The fetched instruction is loaded into the instruction register (IR). An instruction is normally made up of a combination of an operation code and the specification of the operands that present or refer to the data upon which the operation is to be performed. The operation code of the instruction contains bits that specify the action the processor is to take. The processor (or more specifically, the control unit of the processor) interprets the instruction and performs the required action. In general, these actions fall into four categories:
- Processor-memory: data may be transferred from processor to memory, or from memory to processor.
- Processor-I/O: Data may be transferred to or from a peripheral device by initiating a transfer between the processor and an I/O module.
- Data processing: The processor may perform some arithmetic or logic operation on data.
- Control: An instruction may specify that the sequence of execution should be altered.
The execution of an instruction may involve a certain combination of these actions.
An example of the operation of the fetch-execute cycle (cf. Stallings 2018: 33-35)
Let the memory of a virtual machine be organized with 16-bit length (i.e. word-length) memory cells. Each instruction consists of a 4-bit operation code (opcode) and a 12-bit operand. Note that if the operand contains an address, this allows to directly address a maximum of 212=4096 memory cells.
We shall use four hexadecimal digits to represent the 16-bit (one-word) content of the registers, memory addresses and the content of memory cells. (Note that for the 12-bit long addresses three hexadecimal digits would be enough.) Similarly, we shall use one hexadecimal digit to represent the opcode of each instruction.
In the example we want to add two whole numbers represented by two's complement code. We will use one general-purpose register (the accumulator, AC) and three instructions as follows:
- opcode=1: move data from memory into AC:
AC←M(addr) or MOV M(addr), AC
the 12-bit address of the memory cell involved in the operation is specified by the operand 'addr'- opcode=2: move data from AC into memory:
M(addr)←AC or MOV AC, M(addr)
the memory cell involved in the operation is located at the 12-bit address specified by the operand 'addr'- opcode=5: add the content of the memory cell to the content of the accumulator:
AC←M(addr)+AC; or ADD M(addr), AC
the data to be added to the content of AC is located at the address specified by the operand 'addr'; after the addition, the result will be stored in AC (i.e. the sum overwrites the previous content of AC)We assume that the first instruction to be performed is located at the memory address 300 followed sequentially by the further instructions of the program (located at the addresses 301, 302 etc., respectively). Furthermore, we assume that the data that the program manipulates are stored in the memory locations between addresses 940 and 941.
Memory content Address Content (instructions)
0 3 0 0
1 9 4 0
0 3 0 1
5 9 4 1
0 3 0 2
2 9 4 1 (data)
0 9 4 0
0 0 0 3
0 9 4 1
0 0 0 2 Now let's see how the operation is performed in three fetch-execute cycle.
– In the example we analyze in detail the operation of the fetch-execute cycle. Since the initial value of the program or instruction counter register (PC or IP) is set to location 300, in the first cycle the processor will fetch the instruction at the memory location 300 and then immediately increments the value of PC. On the succeeding instruction cycles, the CPU will fetch instructions from locations 301, 302, and so on. (Note, however, that the sequential execution of instructions can be altered at any time by a certain control instruction.)
– In each cycle the fetched instruction is always loaded into the instruction register (IR). The operation code (opcode) of the instruction will specify the necessary action that the processor is to take. After separating the opcode and the operand, the processor (actually, the control unit) interprets the opcode of the instruction and sends control signals to the appropriate units to perform the required action.
1st. cycle Storage unit Value Comment Fetch stage PC
0 3 0 0 fetch the instruction from M(300) M(300)
1 9 4 0 load the content of M(300) into IR IR
1 9 4 0 interpret the instruction
- opcode=1: move memory data into AC
- operand=940: the data is located at M(940)
PC
0 3 0 1 increment the value of PC with 1 Execute stage: AC←M(940) or MOV AC,M(0940) M(940)
0 0 0 3 load the content of M(940) into AC AC
0 0 0 3 store the content of M(940) in AC
2nd. cycle Storage unit Value Comment Fetch stage PC
0 3 0 1 fetch the instruction from M(301) M(301)
5 9 4 1 load the content of M(300) into IR IR
5 9 4 1 interpret the instruction
- opcode=5: add memory data to AC
- operand=941: the data to be added is located at M(941)
PC
0 3 0 2 increment the value of PC with 1 Execute stage: AC←AC+M(941) or ADD M(0941),AC AC
0 0 0 3 add the content of M(941) to AC M(941)
0 0 0 2 AC
0 0 0 5 store the result of the addition in AC
3rd. cycle Storage unit Value Comment Fetch stage PC
0 3 0 2 fetch the instruction from M(302) M(302)
2 9 4 1 load the content of M(302) into IR IR
2 9 4 1 interpret the instruction
- opcode=2: move the content of AC into a memory cell
- operand=941: the memory cell is located at M(941)
PC
0 3 0 3 increment the value of PC with 1 Execute stage: M(941)←AC or MOV M(0941),AC M(941)
0 0 0 2 move the content of AC into M(941) AC
0 0 0 5 M(941)
0 0 0 5 store the content of AC in M(941)
In this example three instruction cycles were needed, each consisting of a fetch stage and an execute stage. As a result, we added the contents of the memory location 940 to the contents of the memory location 941, and then stored the sum at the memory location 941.
The following figure summarizes the process.
Implementation of the above example in Windows
II.1.1. Create and compile C files
In the following, first we write C programs, compile and run them. Then we compile the C programs to assembly programs, analyze their structure, modify them, and finally we compile and run the modified assembly programs.
Table of contents:
Printing "Hello World!" (hello.c)
Open a new 'cmd' window in the c:\temp\ directory and set the default path running the 'setpath' command (only once). Using the notepad hello.c command, create a new file named 'hello.c' with the following content:
#include <stdio.h> int main() { printf("Hello world!\n"); return 0; }Compile, link and run the C program as follows:
Adding 3+2 (simple.c)
Now let us create another simple C program which implements the former example adding two integers together. Open a new 'cmd' window in the c:\temp\ directory, run the notepad simple.c command, and create a new file named 'simple.c' with the following content:
#include <stdio.h> int main() { int a=3; int b=2; b=a+b; return 0; }Compile, link and run the C program as follows:
Note that in the 'cmd' window, we can display the returned value of the 'simple.exe' program using the echo %ERRORLEVEL% command.
For the sake of simplicity, let us create a batch file named 'err.bat' using the notepad err.bat command. It is to contain those two lines:
@echo off
echo %ERRORLEVEL%
With that we created a new command called err which will easily display, if entered, the actual value of the ERRORLEVEL environment variable in the 'cmd' window.
It will be instructive for later considerations that using the GCC compiler we can generate easily the assembly code of the 'simple.c' program (as well as any other C programs). For that purpose, we should enter the gcc simple.c -S -o simple.s command in the 'cmd' window.
The generated assembly program is as follows:
.file "simple.c".text.def __main; .scl 2; .type 32; .endef.globl main.def main; .scl 2; .type 32; .endef .seh_proc mainmain: pushq %rbp.seh_pushreg %rbpmovq %rsp, %rbp.seh_setframe %rbp, 0subq $48, %rsp.seh_stackalloc 48 .seh_endprologuecall __mainmovl $3, -4(%rbp) movl $2, -8(%rbp) movl -4(%rbp), %eax addl %eax, -8(%rbp) movl $0, %eax addq $48, %rsp popq %rbp ret.seh_endproc .ident "GCC: (GNU) 13.2.0"The most important pointers are the quadword-length stack pointer (%rsp) and basic pointer (%rbp). They can be declared in C something like this:
- long long int rsp, rbp;
In the assembly language the local variables are represented as pointers. For example, the doubleword length local variables 'a' and 'b' can be declared in C something like this:
- long int *a, *b;
where the addresses and values of the local variables can be set relatively to the content of the basic ponter:
- a=rbp-4;
- b=rbp-8;
- *a=3;
- *b=2;
Now delete those gray lines from the generated assembly program that are not important to us (because they contain metainformation for the compiler etc.). In addition, move the sum of the operands to the 32-bit accumulator register %eax before the program ends so that we can see the result after the program has run (e.g. by using the previously created 'err.bat' program in the CMD window to display the %ERRORLEVEL% system variable).
The resulting assembly program is as follows (simple.s):
.text .globl main main: pushq %rbp movq %rsp, %rbp subq $48, %rsp movl $3, -4(%rbp) movl $2, -8(%rbp) movl -4(%rbp), %eax addl %eax, -8(%rbp) movl -8(%rbp), %eax /* instead of movl $0, %eax */ addq $48, %rsp popq %rbp retThe explanation of some important parts of the assembly code:
The
pushq %rbpinstruction moves (stores, saves) the content of the %rbp basic pointer register to the top of the stack.The stack is a dedicated and designated part of the memory which can store (and retrieve) data according to the current needs of the programs. In this respect, the push and pop instructions are of most importance for
– adding data to the top of the stack (push), and
– removing, as well as retrieving, data from the top of the stack (pop).The stack consists of a sequence of memory cells to store (and retrieve) data:
![]()
Within the stack, the memory cell with the smallest address is called the top of the stack. The actual address of the top of the stack is always stored in the %rsp stack pointer register. So the function of the %rsp stack pointer register is to point to the top of the stack.The programs use a dedicated register called stack pointer (%rsp) that always points to the top of the stack. It contains the address of the last data item that has been pushed.In order to use a stack
– when a data item is pushed into the stack first the stack pointer is decreased by the size of the data (e.g. by subtracting 8 from the actual value of the stack pointer for a quadword), and then the data is stored at that address;
– when a data item is popped from the stack first the data that is located at the top of the stack is retrieved and stored in the operand of the 'pop' instruction (e.g. in a specified register), and then the stack pointer is increased by the size of the data (e.g. by adding 8 to the actual value of the stack pointer for a quadword).The diagram below illustrates the push and pop operations:
![]()
For example,
(1) pushing the content of the quadword-length (i.e. 8 bytes long) %rbp basic pointer to the top of the stack corresponds to the execution of two instructions:
subq $8, %rsp
movq %rbp, 0(%rsp)
(2) popping the data item from the top of the stack to the the quadword-length (i.e. 8 bytes long) %rbp basic pointer corresponds to the execution of two instructions:
movq 0(%rsp), %rbp
addq $8, %rspLet us simulate the operation of the stack, supposing that in a virtual machine the initial value of %rsp is 1020, and the stack contains random values (generated by the simulation). To use the stack, click on either the 'PUSH' or the 'POP' cell.
Note that the size of the stack is limited is this simulation: the minimum value of the stack pointer is 1000, and the maximum value is 1036.
PUSH POP
stack pointer address 4-byte long memory cells 0996 (out of limit, can't be used) 1000 ? 1004 ? 1008 ? 1012 ? 1016 ? %rsp= 1020 1024 1028 1032 1036 1040 (out of limit, can't be used)
The next two lines of the 'main' section creates a stack frame.
The stack frame is a dedicated and designated part of the stack⇒ which can store local data according to the needs of the currently running programs. Using the terminology of high level programming languages, the stack frame allocates space within the stack for the local variables and parameters of the functions defined in a program (including the 'main' function which is always present).
Each function of the running program has a unique stack frame. The memory cell with the smallest address is called the top of the stack frame. The address of the top of the stack frame is stored in the %rsp stack pointer register. The memory cell with the highest address is called the bottom of the stack frame. The address of the bottom of the stack frame is stored in the %rbp basic pointer register.
The basic pointer (%rbp) register has a special purpose: it points to the bottom of the stack frame of the current function, so local variables can be accessed relative to its value.The created stack frame of the simple.s assembly program is 48 bytes long which is enough to dynamically allocate 12 doubleword length (i.e. 12*4 bytes long) memory space for local variables and parameters as follows:
- the
movq %rsp, %rbpinstruction moves the value of the %rsp stack pointer register (which, as we learned, points to the top of the stack) to the %rbp basic pointer register; from now on it points to the bottom of the stack frame- the
subq $48, %rspinstruction shifts the value of the %rsp stack pointer register by 48 bytes; from now on it points to the top of the stack frame
- the stack can be used as usual; when the next data item is pushed in the stack, the value of the stack pointer will be decreased by the length of the data to be stored, and then the data will be moved to the top of the stack etc.
- as a result, 48 bytes freed in the stack between the contents of %rbp and %rsp; this 48-byte space is called a stack frame
- in the 48-byte long stack frame we can use 12 different local variables and function parameters to store doubleword length data at the following memory addresses (provided that the physical addresses increase from top to bottom):
- −48(%rbp) or 0(%rsp)
- −44(%rbp) or 4(%rsp)
- ...
- −12(%rbp) or 40(%rsp)
- −8(%rbp) or 44(%rsp)
- −4(%rbp) or 48(%rsp)
- note that the bottom of the stack frame is identified by the %rbp basic pointer, and the top of the stack frame is identified by the %rsp stack pointer; but the value of the stack pointer might change later (e.g. if push, pop or call instructions occur), therefore the local variables should always be referenced relatively to the content of the %rbp basic pointer; for example, if the %rbp basic pointer contains the physical address '1000' then
- (%rbp) or 0(%rbp) refers to the address 1000,
- -4(%rbp) refers to the address 1000−4=996,
- -8(%rbp) refers to the address 1000−8=992,
- 4(%rbp) refers to the address 1000+4=1004,
- 8(%rbp) refers to the address 1000+8=1008
- etc.
The other instructions of the simple.s assembly program are as follows.
- the
movl $3, -4(%rbp)instruction assigns ("moves") '3' as a 4-byte length doubleword ("long") decimal value to the local variable at the address [%rbp-4] (which corresponds to the integer type variable 'a' in the simple.c program)- the
movl $2, -8(%rbp)instruction assigns ("moves") '2' as a 4-byte length doubleword ("long") decimal value to the local variable at the address [%rbp-8] (which corresponds to the integer type variable 'b' in the simple.c program)- the
movl -4(%rbp), %eaxinstruction assigns ("moves") the content of the doubleword length local variable at the address [%rbp-4] to the second, "least significant" half of the quadword (8-byte) length accumulator register (%eax) (and fills the first, "most significant" half of the accumulator register with leading zeros)- the
addl %eax, -8(%rbp)instruction
- first adds the content of the second half of the accumulator register (%eax) to the doubleword value of the local variable at the address [%rbp-8],
- then stores the sum in the local variable at the address [%rbp-8] (rewriting its previous content)
- the
addq $48, %rspinstruction destroys the stack frame of 48 bytes and restores the previous value of the %rsp stack pointer register- the
popq %rbpinstruction restores the previously saved content of the %rbp basic pointer register from the top of the stack
- popping (i.e. retrieving, restoring) the content of the quadword-length %rbp basic pointer register from the top of the stack corresponds the execution of two instructions:
movq 0(%rsp), %rbp
addq $8, %rsp
After such considerations, we can easily create the 'simplex.s' assembly program which contains quadword length operands, and returns the sum of the addition (as an %ERRORLEVEL% value):
.text .globl main main: pushq %rbp movq %rsp, %rbp subq $48, %rsp movq $3, -8(%rbp) # local variable a movq $2, -16(%rbp) # local variable b movq -8(%rbp), %rax addq %rax, -16(%rbp) # the sum of a and b movq -16(%rbp), %rax # %ERRORLEVEL% value addq $48, %rsp popq %rbp retNote that in a C program, using quadword-length integer data corresponds to the 'long long int' data type in GCC. So if we wrote a C program with that type of variables, the corresponding assembly program would seem like the program above.
Compile, link and run the assemply program as follows:
Implementation of other examples in Windows
II.1.2. Create and compile C files (continued)
Adding 3+2 with a function (simplef.c)
The aim of the 'simple.c' program can also be implemented using a function named 'sum' which adds two integers together. Open a new 'cmd' window in the c:\temp\ directory, run the notepad simplef.c command, and create a new file named 'simplef.c' with the following content:
#include <stdio.h> int sum(int x,int y) { int temp; temp=x+y; return temp; } int main() { int a=3; int b=2; int c; c=sum(a,b); return c; }Because of the 'return c;' statement, we can display the returned value of the 'simplef.exe' program using the
echo %ERRORLEVEL%
command (or running the 'err' batch file) in the 'cmd' window.Set the default path running the 'setpath' command (remember, only once). Compile, link and run the C program as follows:
Now generate the corresponding 'simplef.s' assembly program with the
gcc simplef.c -S -o simplef.s
command. After deleting the unnecessary lines, we get the following assembly program:.text .globl sum sum: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl %ecx, 16(%rbp) movl %edx, 24(%rbp) movl 16(%rbp), %edx movl 24(%rbp), %eax addl %edx, %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax addq $16, %rsp popq %rbp ret .globl main main: pushq %rbp movq %rsp, %rbp subq $48, %rsp movl $3, -4(%rbp) movl $2, -8(%rbp) movl -8(%rbp), %edx movl -4(%rbp), %eax movl %eax, %ecx call sum movl %eax, -12(%rbp) movl -12(%rbp), %eax addq $48, %rsp popq %rbp ret
There are two important assembly instructions which have to be explained.
The
call suminstruction first pushes the current value of the %rip instruction pointer onto the top of the stack, and then the value of the %rip instruction pointer will be set to the starting address of the 'sum' function. The performed operations can be described by the following instructions:
- %rsp ← %rsp−8
- (%rsp) ← %rip // address of the 'movl %eax, -12(%rbp)' instruction
- %rip ← sum
Although the %rip instruction pointer cannot be directly used in the assembly code, the instructions above "symbolically" corresponds to something like a 'pushq %rip' and 'leaq sum, %rip' (i.e. loading the address 'sum' to the %rip instruction pointer) instructions. Note that the latter instruction has the same result as the 'jmp sum' instruction.
The
retinstruction is always the last instruction of any function. It "pops" the stored address of the next instruction to be executed from the top of the stack (in our case the address of the 'movl %eax, -12(%rbp)' instruction) and restores the value the instruction pointer with that address. The performed operations can be described by the following instructions:
- %rip ← (%rsp) // stored address of the 'movl %eax, -12(%rbp)' instruction
- %rsp ← %rsp+8
Because we modified the value of the %rip instruction pointer, the next fetch-execute cycle will continue the execution of the program immediately after the 'call sum' instruction.
The diagram below illustrates the mechanism of the 'call' and the 'ret' (i.e. return) instructions:
![]()
Note that the called function is named the callee, and the program or function that calls the callee is named the caller.
Let us remember, that the simplef.s assembly program has two functions ('main' and 'sum'), and both functions contain their own stack frame: the 'sum' function creates a 16-byte long stack frame, and the 'main' function creates a 48-byte long stack frame. The doubleword length (32-bit long) contents of the stack of the 'main' function look like this:
- (%rsp) = -48(%rbp) = GROUND_ZERO−48 → (empty)
- ...
- -20(%rbp) → (empty)
- -16(%rbp) = GROUND_ZERO−16 → (empty)
- -12(%rbp) → (address of the variable 'c')
- -8(%rbp) → (address of the variable 'b')
- -4(%rbp) → (address of the variable 'a')
- (%rbp) = GROUND_ZERO → (saved value of %rbp; 8 byte)
- result of the 'pushq %rbp' instruction
- 8(%rbp) → (return address for 'main'; 8 byte)
- the address of the instruction to be executed when the program ends
- ...
We denoted the beginning (or bottom) of the stack frame of the 'main' function by GROUND_ZERO. Note that it is the current value of the %rsp stack pointer register just after the 'push %rbp' instruction has been executed.
When the 'sum' function is called from the 'main' function,
– the address of the next instruction immediately following the 'call sum' instruction in the 'main' function (which is the content of the %rip instruction pointer register) is pushed into the top of the stack,
– the 'sum' function saves the value of the %rbp basic pointer register, and then
– it creates its own 16-byte length stack frame.
After all that operations, the doubleword length (32-bit long) contents of the stack of the 'sum' function look like this:
- (%rsp) = -16(%rbp) = GROUND_ZERO−80 → (empty)
- -12(%rbp) → (empty)
- -8(%rbp) → (empty)
- -4(%rbp) → (empty)
- (after the addition of the 'x' and 'y' parameters, the value of the 'temp' local variable will be stored here)
- (%rbp) = GROUND_ZERO−64 → (saved value of %rbp 8 byte)
- result of the 'pushq %rbp' instruction
- note that the address GROUND_ZERO−64 marks the beginning (or bottom) of the 'sum' function's stack frame
- 8(%rbp) = GROUND_ZERO−56 → (return address for 'sum'; 8 byte)
- the return address for the 'sum' function is the address of the instruction which immediately follows the 'call sum' instruction
- when the 'call sum' instruction is executed, a 'push %rip' instruction is executed before the value of the %rip instruction pointer register is set to the first instruction of the 'sum' function
- when the 'sum' function ends, the 'ret' instruction executes a 'pop %rip' instruction which sets the value of the %rip instruction pointer register to the address of the instruction which immediately follows the 'call sum' instruction
- 16(%rbp) = GROUND_ZERO−48 → value of the first argument or parameter (x)
- the value of the %rcx register is used for passing the initial value to the first argument or parameter of the function
- (the value is placed at the top of the 'main' function's stack frame)
- note that the address GROUND_ZERO−48 marks the top of the 'main' function's stack frame
- 24(%rbp) = GROUND_ZERO−40 → value of the second argument (y)
- the value of the %rdx register is used for passing the initial value to the second argument or parameter of the function
- (the value is placed after the value at the top in the 'main' function's stack frame)
- ...
- (local variables of the 'main' function assigned to the addresses within the 'main' function's stack frame between GROUND_ZERO−12 and GROUND_ZERO−4)
- ...
We created a simulation program⇒ where you can follow the execution of instructions one-by-one, and have a look at the content of the registers and the corresponding part of the memory.
Implementation of other examples in Windows
II.1.3. Create and compile C files (continued)
Adding and printing 3+2 (example.c)
It is not very easy to check the programs if there is no direct visual output in them. So far, we set the return value of the programs to check the output. From now on, we shall use the well-known 'printf' function to display the desired output directly.
Open a new 'cmd' window again in the c:\temp\ directory. Using the notepad example.c command, create a new file named 'example.c' with the following content:
#include <stdio.h> int main() { int a=3; int b=2; int c=a+b; printf("%d + %d = %d\n",a,b,c); return 0; }Set the default path running the 'setpath' command. Compile, link and run the C program as follows:
Like before, we can generate easily the assembly code of the 'example.c' program by entering the gcc example.c -S -o example.s command in the 'cmd' window.
The generated assembly program is as follows:.file "example.c".text.def printf; .scl 3; .type 32; .endef .seh_proc printfprintf: pushq %rbp.seh_pushreg %rbppushq %rbx.seh_pushreg %rbxsubq $56, %rsp.seh_stackalloc 56leaq 48(%rsp), %rbp.seh_setframe %rbp, 48.seh_endprologuemovq %rcx, 32(%rbp) # 4th argument stored movq %rdx, 40(%rbp) # 3rd argument stored movq %r8, 48(%rbp) # 5th argument stored movq %r9, 56(%rbp) # 6th argument storedleaq 40(%rbp), %rax movq %rax, -16(%rbp) movq -16(%rbp), %rbx movl $1, %ecx movq __imp___acrt_iob_func(%rip), %rax call *%rax movq %rax, %rcx movq 32(%rbp), %rax movq %rbx, %r8 movq %rax, %rdx call __mingw_vfprintf movl %eax, -4(%rbp) movl -4(%rbp), %eaxaddq $56, %rsp popq %rbx popq %rbp ret.seh_endproc .def __main; .scl 2; .type 32; .endef.section .rdata,"dr".LC0: .ascii "%d + %d = %d\12\0" .text .globl main.def main; .scl 2; .type 32; .endef .seh_proc mainmain: pushq %rbp.seh_pushreg %rbpmovq %rsp, %rbp.seh_setframe %rbp, 0subq $48, %rsp.seh_stackalloc 48 .seh_endprologue call __mainmovl $3, -4(%rbp) movl $2, -8(%rbp) movl -4(%rbp), %edx movl -8(%rbp), %eax addl %edx, %eax movl %eax, -12(%rbp) movl -12(%rbp), %ecx movl -8(%rbp), %edx movl -4(%rbp), %eax movl %ecx, %r9d movl %edx, %r8d movl %eax, %edx leaq .LC0(%rip), %rax movq %rax, %rcx call printf movl $0, %eax addq $48, %rsp popq %rbp ret.seh_endproc .ident "GCC: (GNU) 13.2.0" .def __mingw_vfprintf; .scl 2; .type 32; .endefNote that the label
.LC0:identifies the format string necessary for the 'printf' function. In the format string the%dformat specifiers tells the 'printf' function that it should print doubleword length (i.e. integer type) variables.Based on the compiled program, we can easily create the 'examplex.s' assembly program which contains only quadword length operands.
Although it is very instructive to see the assembly implementation of the 'printf' function, using 'gcc' we can directly call the 'printf' function from an assembly program.Open a new 'cmd' window in the c:\temp\ directory, run the notepad examplex.s command, and create a new file named 'examplex.s' with the following content:.data .msg: .ascii "%d + %d = %d\12\0" .text .globl main main: pushq %rbp movq %rsp, %rbp subq $48, %rsp movq $3, -8(%rbp) # local variable a movq $2, -16(%rbp) # local variable b movq -8(%rbp), %rdx movq -16(%rbp), %rax addq %rdx, %rax movq %rax, -24(%rbp) # local variable c movq -24(%rbp), %rcx movq -16(%rbp), %rdx movq -8(%rbp), %rax movq %rcx, %r9 # 6th argument (var c) movq %rdx, %r8 # 5th argument (var b) movq %rax, %rdx # 3rd argument (var a) leaq .msg(%rip), %rax movq %rax, %rcx # 4th argument (pattern .msg) call printf movq $0, %rax addq $48, %rsp popq %rbp retCompile, link and run the assembly program as follows:
There is one assembly instruction in the program which have to be explained.
First, the
.msglabel points to the format string of the 'printf' function (containing the %d format specifier which is used for printing integer values). In a so-called RIP-relative addressing assembly technique, this address can be accessed relatively to the %rip instruction pointer which always points to the address of the next instruction, i.e. the instruction that immediately follows the 'leaq' instruction (that has reference to the address). Of course, the compiler can easily count the relative address of the .msg label (called offset).Second, the
leaq .msg(%rip), %rax
instruction moves the address .msg(%rip) into the %rax accumulator register, and then its value is moved to the %rcx register to be transferred as the 4th argument to the 'printf' function.Third, the address .msg(%rip) of the format string is transferred to the 'printf' function through the %rcx register (carrying the 4th argument or parameter of the 'printf' function).
Note again, that the 'printf' function can be called from an assembly program directly.
Before we continue, let us briefly review the available registers of the x86/x64 architecture.
Registers of the Intel x86/x64 architecture
In x86/64 assembly programs, we use a lot of registers to store the necessary data of the program. Therefore it is high time to have an overview which registers are available for the assembly programs in the Intel x86/x64 architecture. First note, that using the AT&T assembly syntax,
– the 32 bit wide register names are prefixed with the %e characters, and
– the 64 bit wide register names are prefixed with the %r characters.Note that when we declare an int type variable in C, its length will be 32 bit (i.e. it is doubleword wide).
In the Intel x86/x64 architecture the detailed list of some important registers is as follows (see e.g. X86-64 Architecture Guide, 2025-03-11; Assembly 1: Basics, 2025-03-30; x64 Architecture Overview and Registers, 2026-02-19):
General-purpose registers: Accumulator Register Purpose Size Saved across calls %raxthe most important temporary register for arithmetic or logic calculations, moving instructions etc. (called accumulator)
return value of a function64 bit No %eax the lower half of the 8 byte wide %rax register 32 bit %ax the lower half of the 4 byte wide %eax register 16 bit %ah the higher half of the 2 byte wide %ax register 8 bit %al the lower half of the 2 byte wide %ax register 8 bit Structure of the accumulator
Note that in memory the least significant byte (LSB) will be stored first (i.e. it will have the smallest address). It is called little-endian order. In case of file storage or network transfer, usually the most significant byte (MSB) will be stored or transferred first (and the least significant byte (LSB) will be the last). This is the opposite of the little-endian order and therefore called big-endian order. For example, if the %rax register contains a 8-byte long hex value 01 23 45 67 89 AB CD EF and its value is moved to the memory address 1000, then the following operations are performed in byte level:
(%rax, 64 bit) (%eax, 32 bit) (%ax, 16 bit) (%ah, 8 bit) (%al, 8 bit) (MSB) (LSB) M(1000) ← EF
M(1001) ← CD
M(1002) ← AB
M(1003) ← 89
M(1004) ← 67
M(1005) ← 45
M(1006) ← 23
M(1007) ← 01Note that a selected part of the memory is usually visualized as moving down (↓) from the cells with lower memory addresses to the cells with higher memory addresses.
General-purpose registers: other registers Register Purpose Size Saved across calls %rbx callee-saved
Formerly we saw⇒ that the 'printf' function saves (push %rbx) the value of the %rbx register before any other instruction is performed, and at the end of the 'printf' function it reloads (pop %rbx) the %rbx register's saved content.64 bit Yes %ebx the lower half of the 8 byte wide %rbx register 32 bit %bx the lower half of the 4 byte wide %ebx register 16 bit %bh the higher half of the 2 byte wide %bx register 8 bit %bl the lower half of the 2 byte wide %bx register 8 bit %rcx used to pass 4th argument to functions 64 bit No %ecx the lower half of the 8 byte wide %rcx register 32 bit %cx the lower half of the 4 byte wide %ecx register 16 bit %ch the higher half of the 2 byte wide %cx register 8 bit %cl the lower half of the 2 byte wide %cx register 8 bit %rdx used to pass 3rd argument to functions 64 bit No %edx the lower half of the 8 byte wide %rdx register 32 bit %dx the lower half of the 4 byte wide %edx register 16 bit %dh the higher half of the 2 byte wide %dx register 8 bit %dl the lower half of the 2 byte wide %dx register 8 bit %rsi used to pass 2nd argument to functions 64 bit No %esi the lower half of the 8 byte wide %rsi register 32 bit %si the lower half of the 4 byte wide %esi register 16 bit %sil the lower half of the 2 byte wide %si register 8 bit %rdi used to pass 1st argument to functions 64 bit No %edi the lower half of the 8 byte wide %rdi register 32 bit %di the lower half of the 4 byte wide %edi register 16 bit %dil the lower half of the 2 byte wide %di register 8 bit %r8
used to pass 5th argument to functions
64 bit No
%r8d the lower half of the 8 byte wide %r8 register 32 bit %r8w the lower half of the 4 byte wide %r8d register 16 bit %r8b the lower half of the 2 byte wide %r8w register 8 bit %r9
used to pass 6th argument to functions
64 bit No
%r9d the lower half of the 8 byte wide %r9 register 32 bit %r9w the lower half of the 4 byte wide %r9d register 16 bit %r9b the lower half of the 2 byte wide %r9w register 8 bit %r10 temporary 64 bit No
%r11 temporary 64 bit No
%r12 callee-saved 64 bit Yes
%r13 callee-saved 64 bit Yes
%r14 callee-saved 64 bit Yes
%r15 callee-saved 64 bit Yes
Special-purpose registers Register Purpose Size Saved across calls %rspstack pointer 64 bit Yes %esp the lower half of the 8 byte wide %rsp register 32 bit %sp the lower half of the 4 byte wide %esp register 16 bit %spl the lower half of the 2 byte wide %sp register 8 bit %rbpbase pointer; callee-saved 64 bit Yes %ebp the lower half of the 8 byte wide %rbp register 32 bit %bp the lower half of the 4 byte wide %ebp register 16 bit %bpl the lower half of the 2 byte wide %bp register 8 bit %ripinstruction pointer or program counter 64 bit (call↔ret) %eip the lower half of the 8 byte wide %rip register 32 bit %ip the lower half of the 4 byte wide %eip register 16 bit %rflagsstatus or control flags 64 bit No %eflags the lower half of the 8 byte wide %rflags register 32 bit %flags the lower half of the 4 byte wide %eflags register 16 bit The status (or flags) register contains mostly one-bit storage units ("flags") that reflect the current state of an x86/x64 CPU. For example, some flags show some important characteristics of the result of arithmetic or logical operations (including comparisons etc.). Some usual flags are illustrated below within a 64-bit %rflags register:
63 ... 11 ... 7 6 5 4 3 2 1 0 OF SF ZF AF PF CF The flag names are abbreviated as follows:
- CF: Carry Flag (CF=1 when an arithmetic carry has been generated)
- PF: Parity Flag (PF=1 indicates that the number of 1 bits within the least significant byte of the result of the last operation is even; otherwise, PF=0 indicates that the number of 1 bits is odd)
- AF: Auxiliary Carry Flag (AF=1 when an arithmetic carry has been generated using binary-coded decimal (BCD) arithmetic)
- ZF: Zero Flag (the zero flag is a central feature on most conventional CPU architectures: it is used to check the result of an arithmetic operation, including comparisons; ZF=1 if the result of the operation is zero, otherwise ZF=0)
- SF: Sign Flag or Negative Flag (SF=1 indicates that the result of the last mathematical operation produced value 1 in the most significant bit (MSB) position (i.e. the leftmost or sign bit of the result was set indicating negative value)
- OF: Overflow Bit (OF=1 shows that an overflow has occurred in the last arithmetic operation)
- Note that in two's complement coding, the operation C=A+B produces an overflow if the
(SA∧SB∧⌝SC) ∨ (⌝SA∧⌝SB∧SC)
logical expression is true (where SA is the sign bit of the operand 'A' etc.). Obviously, the sum of two negative numbers cannot be positive, and the sum of two positive numbers cannot be negative.Formerly (e.g. in the mainframe age) the program counter and the status register were collectively called PSW (program status word) register. Nevertheless, this term can also be used for modern computers as well, including, in the first place, the content of the IP and the flags registers. "The PSW contains status information about the currently running process, including memory usage information, condition codes, and other status information such as an interrupt enable/disable bit and a kernel/user-mode bit." (Stallings 2018: 41)
Implementation of other examples in Windows
II.2. Create and compile assembly files
Table of contents:
Printing "Hello World!" (asmh.s)
Now let us create a simple program in Intel x86/x64 assembly language which displays the well-known 'Hello world!' message. Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad asmh.s command, and create a new file named 'asmh.s' with the following content:
.globl main // definitions of constants and variables .data hello: .ascii "Hello world!\12\0" // program instructions (code) .text main: pushq %rbp movq %rsp, %rbp subq $32, %rsp leaq hello(%rip), %rax /* setting the parameter for the function 'printf' */ movq %rax, %rcx # address of 'hello' call printf /* displayed 'Hello world!' */ movl $0, %eax # set ERRORLEVEL value addq $32, %rsp popq %rbp retCompile, link and run the assembly program as follows:
Note that the size of the stack frame is 32 bytes, even though there are no local variables in the program. The four quadwords allocated at the top of the stack frame can be used for the (possible) parameters of the 'printf' function.Setting the ERRORLEVEL (abc.s)
After we have successfully created and compiled the 'asmh.s' program, let us create another simple program in Intel x86/x64 assembly language which does nothing except returns the value 10 as an ERRORLEVEL value.
Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad abc.s command, and create a new file named 'abc.s' with the following content:
.globl main main: enter $0, $0 movq $10, %rax leave retNote that the first two instructions of the 'main' function creates a stack frame which, among others, can contain the values of the local variables (if there are any such variables at all). The basic pointer register (%rbp) is used as a reference to point to the address of those local variables (i.e. the local variables can be addressed relatively to the value of the basic pointer).
The 'enter $0, $0' assembly instruction corresponds to the
pushq %rbpinstructions. It creates the stack frame of the function.
movq %rsp, %rbp
Note that e.g. the 'enter $24, $0 instruction would allocate in the stack a 24 byte long memory space by subtracting 24 from the actual value of the stack pointer. Because 6*4=24 holds, this would be enough for six doubleword (i.e. 4 byte=32 bit) length local variables (or for three quadword length local variables, respectively).
The 'leave' assembly instruction corresponds to the
movq %rbp, %rspinstructions. It frees (or destroys) the stack frame of the function.
popq %rbp
Using the push / pop instructions instead of the enter / leave instructions, we can create another version of the program 'abc.s' as follows:
.globl main main: pushq %rbp movq %rsp, %rbp movq $10, %rax movq %rbp, %rsp popq %rbp retCompile, link and run the assembly program:
Here, like in the case of the 'simple.exe' program⇒ or the 'simplex.exe' program,⇒ we can display the returned value of the 'abc.exe' program using the echo %ERRORLEVEL% command in the 'cmd' window (or we can enter the 'err' command⇒ if the 'err.bat' file exists).
Adding 3+2 (abcs.s)
Now let us create an equivalent of the 'simple.c' program in Intel x86/x64 assembly language which adds two numbers (3 and 2) together as long integer types, stores the sum in another longint variable, and returns the sum as an ERRORLEVEL value. Before that, the program will warn us to check the actual value of the ERRORLEVEL environment variable.
Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad abcs.s command, and create a new file named 'abcs.s' with the following content:
.globl main .data hello: .ascii "\12See the ERRORLEVEL value!\12\0" .text main: pushq %rbp movq %rsp, %rbp subq $56, %rsp /* stack frame created */ movq $3, -8(%rbp) movq $2, -16(%rbp) movq -8(%rbp), %rax addq -16(%rbp), %rax movq %rax, -24(%rbp) leaq hello(%rip), %rcx call printf movq -24(%rbp), %rax # set ERRORLEVEL value /* stack frame to be destroyed */ addq $56, %rsp popq %rbp retCompile, link and run the assembly program as follows:
Here, like in the case of the 'simple.exe' and 'abc.exe' programs, in the 'cmd' window we can display the returned value of the 'abcs.exe' program using the echo %ERRORLEVEL% or simply the err command. But in this case it returns the sum of the addition 3+2 (i.e. 5).
The size and content of the stack frame needs some explanation. The size of the stack frame is 56 bytes which corresponds to 7 quadwords (i.e. 56=7*8). The structure and content of the stack frame is as follows:
address content 0(%rsp) -56(%rbp) parameters for the function 'printf' 8(%rsp) -48(%rbp) 16(%rsp) -40(%rbp) 24(%rsp) -32(%rbp) -24(%rbp) variable c -16(%rbp) variable b -8(%rbp) variable a 56(%rsp) 0(%rbp) previous value of %rbp (pushed by the first instruction of main) 8(%rbp) return address for the caller of 'main' (for 'ret' in main) The basic pointer (%rbp) register has a special purpose: it points to the bottom of the stack frame of the current function, so local variables can be accessed relative to its value.As for the last row of the table which belongs to the address 8(%rbp) just below the bottom of the stack frame, when the program environment (i.e. the cmd.exe in our case) runs the abcs.exe program, it calls the 'main' global function of the abcs.exe program, and the current value of the instruction pointer is automatically pushed onto the top of the stack. (Thus when the called 'main' function exits and returns, the CPU can continue the execution of the caller program by popping the address of the next instruction to be performed from the stack and loading it into the instruction pointer).
In general, when a specific function of the program is called by another function (from the same or from another program), the return address of the next instruction to be executed after the 'call' instruction is automatically pushed onto the top of the stack.
Note that in the fetch-execute cycle the address of the next instruction is always stored in the %rip instruction pointer or program counter register. Thus the 'call' function, when executed, pushes the current value of the instruction pointer onto the top of the stack. After that the called function pushes the value of the basic pointer into the stack and creates its stack frame.
Listing the first 10 natural numbers
First, let us see a C program that prints the first 10 natural numbers (starting with 1, then 2, 3, 4, ..., 10). Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad natural.c command, and create a new file named 'natural.c' with the following content:
#include <stdio.h> int main() { int x=1; int i, n=10; for(i=1;i<=n;i++) { printf("%d\n",x); x++; } return i; }Compile, link and run the compiled C program as follows:
Now it can be very instructive to see the compiled assembly version of the C program. Type and run in the 'cmd' window the gcc natural.c -S -o nat.s command. After making some changes (omitting some parts, commenting some of the instructions etc.), the resulting file will look like something like this:
.data .pattern: .ascii "%d\12\0" .text printf: pushq %rbp pushq %rbx # callee saved subq $56, %rsp leaq 48(%rsp), %rbp /* 56=48+8; pushing %rbx allocates +8 bytes at the stack⇒ */ movq %rcx, 32(%rbp) # 4th argument stored movq %rdx, 40(%rbp) # 3rd argument stored movq %r8, 48(%rbp) # 5th argument stored movq %r9, 56(%rbp) # 6th argument storedleaq 40(%rbp), %rax movq %rax, -16(%rbp) # local variable movq -16(%rbp), %rbx movl $1, %ecx movq __imp___acrt_iob_func(%rip), %rax call *%raxmovq %rax, %rcx movq 32(%rbp), %rax movq %rbx, %r8 movq %rax, %rdx call __mingw_vfprintf movl %eax, -4(%rbp) movl -4(%rbp), %eaxaddq $56, %rsp popq %rbx popq %rbp ret .text .globl main main: pushq %rbp movq %rsp, %rbp subq $48, %rsp # stack frame (48 bytes)call __mainmovl $1, -4(%rbp) # variable x movl $10, -12(%rbp) # variable n movl $1, -8(%rbp) # variable i jmp .L4 /* begin of loop */ .L5: movl -4(%rbp), %eax # variable x to print movl %eax, %edx # 3rd parameter (printf) leaq .pattern(%rip), %rax # copy address movq %rax, %rcx # 4th parameter (printf) call printf addl $1, -4(%rbp) # x++ addl $1, -8(%rbp) # i++ .L4: movl -8(%rbp), %eax # i -> %eax cmpl -12(%rbp), %eax # i≤n ? jle .L5 # jump if i≤n /* end of loop */ movl -8(%rbp), %eax addq $48, %rsp popq %rbp retBefore the 'printf' function is called and the local variables are "declared" (i.e. before the 'jmp .L4' instruction), the content of the stack frame created by the 'main' function is as follows:
address content 0(%rsp) -48(%rbp) allocated space for the four parameters (or arguments) for the function 'printf' 8(%rsp) -40(%rbp) 16(%rsp) -32(%rbp) 24(%rsp) -24(%rbp) (not used) -12(%rbp) variable n (initially n=10) -8(%rbp) variable i (initially i=1) -4(%rbp) variable x (initially x=1) 48(%rsp) 0(%rbp) previous value of %rbp (pushed by the first instruction of main) 8(%rbp) return address for the caller of 'main' (for 'ret' in main) After the 'printf' function is called for the first time by the 'main' function, the content of the stack frame created by the 'printf' function is as follows:
address content 0(%rsp) -48(%rbp) ... ... ... 48(%rsp) 0(%rbp) 56(%rsp) 8(%rbp) previous value of %rbx (pushed by the second instruction of printf) 16(%rbp) previous value of %rbp (pushed by the first instruction of printf) 24(%rbp) return address for the caller of 'printf', i.e. the address of the next instruction of 'main' after the 'call printf' instruction (the following part of the stack is the same space for the arguments (or parameters) of the 'printf' function that has been allocated by the 'main' function, see the top of its stack frame in the table above) 32(%rbp) 4th argument of the function 'printf' 40(%rbp) 3rd argument of the function 'printf' 48(%rbp) 5th argument of the function 'printf' 56(%rbp) 6th argument of the function 'printf' Let us now create an equivalent of the above program in assembly language which prints the first 10 natural numbers and uses quadword length local variables. Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad natural.s command, and create a new file named 'natural.s' with the following content:
.data msg: .ascii "The first %d natural numbers:\12\0" pattern: .ascii "%d\12\0" .text .globl main main: pushq %rbp movq %rsp, %rbp subq $64, %rsp movl $1, -4(%rbp) # x movl $10, -12(%rbp) # n movl $1, -8(%rbp) # i leaq msg(%rip), %rcx movl -12(%rbp), %edx call printf .L0: movl -8(%rbp), %eax cmpl -12(%rbp), %eax # i>n ? jg .L1 leaq pattern(%rip), %rcx movl -4(%rbp), %edx call printf incl -4(%rbp) incl -8(%rbp) jmp .L0 .L1: movl $0, %eax # return value addq $64, %rsp popq %rbp retCompile, link and run the assembly program as follows:
Listing the first 10 even numbers
Using the assembly instructions we have learned so far, we can easily list the first even numbers. Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad even.s command, and create a new file named 'even.s' with the following content:
.data .msg: .ascii "i=%d, x=%d, n=%d\12\0" .text .globl main main: pushq %rbp movq %rsp, %rbp // pushq %rbx subq $64, %rsp movq %rbx, -8(%rbp) movq $1, -16(%rbp) # local variable i (loop variable) movq $0, -24(%rbp) # local variable x movq $10, -32(%rbp) # local variable n loop: movq -32(%rbp), %r9 # 4th argument (var n) movq -24(%rbp), %r8 # 3th argument (var x) movq -16(%rbp), %rdx # 2rd argument (var i) leaq .msg(%rip), %rcx call printf movq -24(%rbp), %rbx addq $2, %rbx movq %rbx, -24(%rbp) movq -16(%rbp), %rax addq $1, %rax movq %rax, -16(%rbp) cmpq -32(%rbp), %rax # compare 'n' with 'i' jle loop # jump if i<=n movq -8(%rbp), %rbx movl $0, %eax addq $64, %rsp // popq %rbx popq %rbp retNote that there is one important solution in the above assembly program. We used the %rbx general-purpose register which should be callee-saved. But saving its value by the 'push' and 'pop' instructions caused some problems in our environment (using Windows 11 and gcc-13.2.0, 2026-03-05). So we simply saved the content of the %rbx register as the first quadword in the stack frame of the 'main' function, using the movq %rbx, -8(%rbp) instruction.
Listing the first 10 powers of 2
First, let us see a C program that prints the first 10 powers of 2 (starting with 1, then 2, 4, 8 etc.). Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad powers.c command, and create a new file named 'powers.c' with the following content:
#include <stdio.h> int nextpow(int x) { int p=x+x; return p; } int main() { int x=1; int i=1, n=10; do { printf("%d\n",x); x=nextpow(x); i++; } while(i<=n); return i; }Compile, link and run the compiled C program as follows:
Let us now create an equivalent of the 'powers.c' program in Intel x86/x64 assembly language which prints the first 10 powers of 2. Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad powers.s command, and create a new file named 'powers.s' with the following content:
.globl main .data pattern: .ascii "%d\12\0" .text nextpow: pushq %rbp movq %rsp, %rbp subq $16, %rsp # stack frame size movl %ecx, 16(%rbp) # parameter x movl 16(%rbp), %eax addl %eax, %eax movl %eax, -4(%rbp) # local variable p movl -4(%rbp), %eax addq $16, %rsp popq %rbp ret main: pushq %rbp movq %rsp, %rbp subq $48, %rsp movl $1, -4(%rbp) # variable x movl $1, -8(%rbp) # variable i movl $10, -12(%rbp) # variable n .loop: movl -4(%rbp), %edx leaq pattern(%rip), %rcx call printf movl -4(%rbp), %ecx call nextpow movl %eax, -4(%rbp) addl $1, -8(%rbp) # i++ movl -8(%rbp), %eax cmpl -12(%rbp), %eax # i<=n ? jle .loop movl -8(%rbp), %eax addq $48, %rsp popq %rbp retCompile, link and run the assembly program as follows:
Listing the first 10 factorials
First, let us see a C program that prints the first 10 factorials (starting with 1, then 2, 6, 24 etc.). Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad fact.c command, and create a new file named 'fact.c' with the following content:
#include <stdio.h> int f(int n) { int temp; temp=1; for(int i=2;i<=n;i++) { temp=temp*i; } return temp; } int main() { int n=10; printf("List of the first %d factorials:\n",n); int i=1; while(i<=n) { printf("%d\n",f(i)); i=i+1; }; return i; }Compile, link and run the compiled C program as follows:
Let us now create an equivalent of the 'fact.c' program in Intel x86/x64 assembly language which prints the first 10 factorials. Open a new 'cmd' window in the c:\temp\gcc directory, run the notepad fact.s command, and create a new file named 'fact.s' with the following content:
.globl main .globl f .data .LC0: .ascii "List of the first %d factorials:\12\0" .LC1: .ascii "%d\12\0" .text f: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl %ecx, 16(%rbp) movl $1, -4(%rbp) movl $2, -8(%rbp) jmp .L4 .L5: movl -4(%rbp), %eax imull -8(%rbp), %eax movl %eax, -4(%rbp) addl $1, -8(%rbp) .L4: movl -8(%rbp), %eax cmpl 16(%rbp), %eax jle .L5 movl -4(%rbp), %eax addq $16, %rsp popq %rbp ret main: pushq %rbp movq %rsp, %rbp subq $48, %rsp movl $10, -8(%rbp) movl -8(%rbp), %eax movl %eax, %edx leaq .LC0, %rax movq %rax, %rcx call printf movl $1, -4(%rbp) jmp .L8 .L9: movl -4(%rbp), %eax movl %eax, %ecx call f movl %eax, %edx leaq .LC1, %rax movq %rax, %rcx call printf addl $1, -4(%rbp) .L8: movl -4(%rbp), %eax cmpl -8(%rbp), %eax jle .L9 movl -4(%rbp), %eax addq $48, %rsp popq %rbp retNote that the imul assembly instruction executes a signed multiplication or product of the first operand (which can be either a register or a word-length or doubleword-length memory content) and the second operand (a register), and stores the resulting product in the the register specified as the second operand.
Compile, link and run the assembly program as follows:
Review questions and exercises
Questions:
- What are the main hw elements of the Von Neumann architecture?
- What are the main components of the CPU, and what are their basic functions?
- What units does the main memory consist of and how can they be identified?
- What is buffer memory and why is it used?
- Illustrate and explain briefly the operation of the instruction (or the fetch-execute) cycle!
- What is the main purpose of the PC (or IP) and the IR registers?
- List the 64-bit length general-purpose registers and their symbolic notations in the x86/x64 assembly language!
- List the 64-bit length special-purpose registers and their symbolic notations in the x86/x64 assembly language!
Create programs in x86/x64 assembly language which performs the following tasks:
- Add two integers (e.g. 5 and 8) together using a function named 'addint'. Print the result in the following form: 5 + 8 = 13. Set the return value of the main function to the sum of the addition.
- use 64-bit length registers and memory locations for the local variables
- use 32-bit length registers and memory locations for the local variables
- Subtract two integers (e.g. 5 and 8) together using a function named 'subint'. Print the result in the following form: 5 − 8 = 13. Set the return value of the main function to the sum of the subtraction.
- use 64-bit length registers and memory locations for the local variables
- use 32-bit length registers and memory locations for the local variables
- List the first 'n' elements (e.g. n=10) of a given number sequence (e.g. 2, 4, 6, 8, ...) with their sequence number in the following form:
--------------
1. element = 2
2. element = 4
...
10. element = 20
--------------
The given number sequence can be as follows:Each program should start with printing its function and end with the name of the programmer (as well as the current date).