0% found this document useful (0 votes)
8 views76 pages

Module 3 - Part 1

The document discusses memory management in operating systems, detailing how it allocates, tracks, and reclaims memory for processes. It explains the concept of address space, the structure of memory segments (code, heap, stack), and the importance of memory APIs for safe memory operations. Additionally, it highlights common errors in memory allocation and deallocation, such as memory leaks and buffer overflows.

Uploaded by

visakh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views76 pages

Module 3 - Part 1

The document discusses memory management in operating systems, detailing how it allocates, tracks, and reclaims memory for processes. It explains the concept of address space, the structure of memory segments (code, heap, stack), and the importance of memory APIs for safe memory operations. Additionally, it highlights common errors in memory allocation and deallocation, such as memory leaks and buffer overflows.

Uploaded by

visakh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

1

[Link]
Professor, CSE, UEC

[Link], CSE 06-02-2026


 Memory management is a core function of an OS,
 It controls and coordinates the use of a computer’s main
memory (RAM).
 It ensures that memory is efficiently allocated to processes,
used properly during execution, and released when no
longer needed.
 The OS acts as a manager between programs and physical
memory,
 deciding which process gets how much memory, when, and
for how long.
[Link], CSE 06-02-2026 2
 Keep track of the resource (memory). What parts are in use
and by whom? What parts are not in use (called free)?
 If multiprogramming, decide which process gets memory,
when it gets it, and how much.
 Allocate the resource (memory) when the processes request
it.
 Reclaim the resource (memory) when the process no longer
needs it or has been terminated.

[Link], CSE 06-02-2026 3


 Address space is an abstraction of memory provided to a
running program.
 It represents the set of memory addresses that a process can
access during its execution.
 Instead of directly interacting with physical memory, a
program operates within its own logical view of memory,
which simplifies programming and improves system safety.
 Each process in a system is given a separate address space,
ensuring that one process cannot interfere with the memory
of another.
 This isolation is essential for system stability and security.
[Link], CSE 06-02-2026 4
 Figure 13.3 illustrates a
process address space,
 which is the logical view of
memory provided by the os to
a running program.
 Physical memory may be
located elsewhere,
 the program assumes that it
occupies a continuous block
of memory ranging from 0 KB
to 16 KB.
[Link], CSE 06-02-2026 5
1. Program Code Segment (0 KB – 1 KB)
 Located at the lowest memory addresses
 Contains the executable instructions of the program
 This segment is static, meaning its size does not change during
execution
 It is usually read-only to prevent accidental modification

2. Heap Segment (starts at 1 KB)


 Used for dynamic memory allocation
 Memory requested at runtime (e.g., using malloc() or new) is
allocated here
 In the given figure, the heap grows downward
 Managed explicitly by the programmer
[Link], CSE 06-02-2026 6
3. Free Space (Middle Region)
 The shaded region represents unused or free memory
 This space allows both the heap and the stack to grow dynamically
 Efficient memory management ensures that these two regions do
not collide

4. Stack Segment (starts at 16 KB)


 Located at the highest memory addresses
 Stores:
 Local variables, Function parameters, Return addresses
 The stack grows upward as function calls increase
 Automatically managed by the system
[Link], CSE 06-02-2026 7
 The heap and stack grow in opposite directions
 This arrangement maximizes memory utilization and
flexibility
 If the heap and stack meet, a stack overflow or heap overflow
may occur
 The addresses shown (0 KB to 16 KB) are virtual addresses
 The os, using hardware support (MMU), maps these virtual
addresses to physical memory locations
 This mechanism enables memory virtualization, protection,
and process isolation
[Link], CSE 06-02-2026 8
 The figure illustrates how
multiple processes and the os
are stored in main memory
(RAM) at the same time.
 This arrangement
demonstrates
multiprogramming,
 where several processes
coexist in memory and share
system resources.

[Link], CSE 06-02-2026 9


1. Operating System in Memory
 OS occupies the lowest portion of memory (from 0 KB to 64
KB).
 This region contains OS code, data structures, and system
routines.
 The OS remains resident in memory to manage processes,
memory, and hardware.

[Link], CSE 06-02-2026 10


2. User Processes in Memory
 remaining memory is divided among three user processes:
 Process C
 Loaded between 128 KB and 192 KB
 Contains its own code, data, stack, and heap
 Process B
 Loaded between 192 KB and 256 KB
 Completely isolated from other processes
 Process A
 Loaded between 320 KB and 384 KB
 Runs independently in its own allocated region
[Link], CSE 06-02-2026 11
 Each process is placed at a different physical memory location,
 but each believes it starts at address 0 in its own virtual address
space.

3. Free Memory Regions


 The shaded areas represent free (unused) memory
 These gaps are created due to:
 Process termination
 Variable-sized memory allocation
 Such gaps may lead to external fragmentation in contiguous
memory allocation systems
[Link], CSE 06-02-2026 12
 Each process assumes it is loaded at address 0
 The OS and hardware translate these virtual addresses to
the actual physical addresses shown
 This abstraction is known as memory virtualization

[Link], CSE 06-02-2026 13


 Memory API is a collection of system calls and library
functions that allow programs to allocate, access, protect, share,
and release memory.
 These APIs provide a safe interface between applications and
the operating system’s memory management subsystem.

Why Memory API is Needed?


 Programs need memory at runtime
 Physical memory details must be hidden from applications
 Ensures protection and isolation
 Supports virtual memory
[Link], CSE 06-02-2026 14
 In running a C program, there are two types of memory that are
allocated.
 The first is called stack memory, and allocations and deallocations of
it are managed implicitly by the compiler, for this reason it is
sometimes called automatic memory.
 Declaring memory on the stack in C is easy.
 For example, let’s say you need some space in a function func() for an
integer, called x.
 To declare such a piece of memory, you just do something like this:
void func() {
int x; // declares an integer on the stack
...
}
[Link], CSE 06-02-2026 15
 The compiler does the rest, making sure to make space on the stack
when you call into func().
 When your return from the function, the compiler deallocates the
memory.

 Second type of memory, called heap memory, where all allocations


and deallocations are explicitly handled by you, the programmer.
 Example of how one might allocate a pointer to an integer on the
heap:
void func() {
int *x = (int *) malloc(sizeof(int));
...
}
[Link], CSE 06-02-2026 16
 First, you might notice that both stack and heap allocation
occur on this line:
 first the compiler knows to make room for a pointer to an
integer when it sees (int *x);
 subsequently, when the program calls malloc(), it requests
space for an integer on the heap;
 the routine returns the address of such an integer (upon
success, or NULL on failure), which is then stored on the stack
for use by the program.

[Link], CSE 06-02-2026 17


 malloc() (memory allocation) is a standard library function used
to dynamically allocate memory at runtime.
 It allocates a specified number of bytes from the heap of a
process and returns a pointer to the beginning of the allocated
memory block.
 Syntax
void* malloc(size_t size);
 The program calls malloc(size)
 malloc() requests memory from the heap
 The os checks whether sufficient memory is available
 If successful, malloc() returns a pointer to the allocated memory
 If allocation fails, it returns NULL
[Link], CSE 06-02-2026 18
 The allocated memory:
 Is contiguous
 Is uninitialized (contains garbage values)
 Remains allocated until it is explicitly freed

Example
int *arr;
arr = (int*) malloc(10 * sizeof(int));
if (arr == NULL)
{ // Memory allocation failed
}
[Link], CSE 06-02-2026 19
 To free heap memory that is no longer in use, programmers
simply call free():
int *x = malloc(10 * sizeof(int));
...
free(x);
 The routine takes one argument, a pointer that was returned by
malloc().
 the size of the allocated region is not passed in by the user,
 and must be tracked by the memory-allocation library itself.

[Link], CSE 06-02-2026 20


 There are a number of common errors that arise in the use of
malloc() and free().

1. Forgetting To Allocate Memory


 Many routines expect memory to be allocated before you call them.
 For example, the routine strcpy(dst, src) copies a string from a source
pointer to a destination pointer.
 However, if you are not careful, you might do this:
char *src = "hello";
char *dst; // oops! unallocated
strcpy(dst, src); // segfault and die
 When you run this code, it will likely lead to a segmentation fault3

[Link], CSE 06-02-2026 21


 In this case, the proper code might instead look like this:
char *src = "hello";
char *dst = (char *) malloc(strlen(src) + 1);
strcpy(dst, src); // work properly

[Link], CSE 06-02-2026 22


2. Not Allocating Enough Memory (Buffer Overflow)
 Not allocating enough memory occurs when the destination
buffer is smaller than the data being copied into it.
 This often leads to a buffer overflow,
 which causes undefined behavior such as crashes, data corruption,
or security vulnerabilities.
 Example:
char *src = "hello";
char *dst = (char *) malloc(strlen(src)); // too small!
strcpy(dst, src); // ERROR
[Link], CSE 06-02-2026 23
 Correct Code ✅
char *dst = (char *) malloc(strlen(src) + 1);
if (dst == NULL)
{ // handle error}
strcpy(dst, src);

[Link], CSE 06-02-2026 24


3. Forgetting to Initialize Allocated Memory
 Forgetting to initialize allocated memory occurs when a
program correctly allocates memory using malloc(),
 but fails to assign valid values to the allocated memory before
reading or using it.
 Since malloc() does not initialize memory, the contents of the
allocated region are undefined.
 Correct code:
int *arr = malloc(5 * sizeof(int));

[Link], CSE 06-02-2026 25


4. Forgetting to Free Memory
 means allocating memory using malloc()
 but not releasing it using free() after it is no longer needed.
 This problem is called a memory leak.
What is a Memory Leak?
 Memory is allocated but never returned to the system
 Over time, unused memory keeps increasing
 Eventually, the system runs out of memory
Example:
int *p = malloc(100 * sizeof(int));
[Link], CSE free(p); 06-02-2026 26
5. Freeing Memory Before You Are Done With It
 Sometimes a program will free memory before it is finished using it;
 Such a mistake is called a dangling pointer
 The subsequent use can crash the program, or overwrite valid
memory
 (e.g., you called free(), but then called malloc() again to allocate
something else, recycles the errantly-freed memory).
 Example:
free(p);
p = malloc(sizeof(int)); // memory may be reused

[Link], CSE 06-02-2026 27


6. Freeing Memory Repeatedly
 Freeing memory repeatedly means calling free() more than
once on the same memory block.
 This error is known as a double free.
 A double free causes undefined behavior, because once
memory is freed, it no longer belongs to the program.
 Example code:
int *p = malloc(sizeof(int));
free(p); // first free – OK
free(p); // second free – ERROR (double free)
[Link], CSE 06-02-2026 28
7. Calling free() Incorrectly
 Calling free() incorrectly means passing a pointer to free()
 that was not returned by malloc() (or calloc() / realloc()).
 This is called an invalid free, and it leads to undefined behavior.
 Example code:
int *p = malloc(10 * sizeof(int));
free(p + 1); // ERROR
Correct usage:
int *p = malloc(sizeof(int));
free(p);
p = NULL;
[Link], CSE 06-02-2026 29
 Address translation is the process by which the CPU-
generated virtual (logical) address
 is converted into a physical address in main memory by the
hardware (MMU – Memory Management Unit).

Note:
 Programs never access physical memory directly.
 They use virtual addresses, and the OS + hardware handle the
conversion.

[Link], CSE 09-02-2026 30


Why Address Translation is Needed
 Each process thinks it has its own private memory
 Protects one process from accessing another’s memory
 Allows efficient memory sharing and relocation
 Supports virtual memory

[Link], CSE 09-02-2026 31


 Imagine there is a process whose
address space as indicated in Figure
15.1.
 loads a value from memory,
increments it by 3, and then stores the
value back into memory.

 Consider the C statement:


x = x + 3;

[Link], CSE 09-02-2026 32


 The compiler converts this into the following x86 assembly:
 128: movl 0x0(%ebx), %eax; load x into eax
 132: addl $0x03, %eax; add 3
 135: movl %eax, 0x0(%ebx); store back to x

Assumptions:
 Register EBX contains the address of variable x
 Variable x is located at 15 KB in the process’s address space
 Instructions start at address 128

[Link], CSE 09-02-2026 33


Process Address Space Layout (Virtual)
 From the process’s point of view:
 Address space starts at 0
 Maximum size = 16 KB
 Code segment near the top
 Stack near the bottom
 Example values:
 Instruction at address 128
 Variable x at address 15 KB
 Initial value of x = 3000
 Important: All addresses used by the program (128, 132, 135, 15 KB)
are virtual addresses.
[Link], CSE 09-02-2026 34
Memory Accesses Generated by the Program
 When the program runs, it generates the following virtual memory
accesses:
1. Fetch instruction at virtual address 128
2. Load data from virtual address 15 KB
3. Fetch instruction at virtual address 132
4. Execute add (no memory access)
5. Fetch instruction at virtual address 135
6. Store data to virtual address 15 KB
7. From the program’s perspective, everything is within 0 → 16 KB.

[Link], CSE 09-02-2026 35


 However, to virtualize memory, the OS wants to place the process
somewhere else in physical memory, not necessarily at address 0.
 The Core Problem
 OS does NOT want to load every process starting at physical address 0.

 In the example: OS occupies 0–16 KB


 Process is placed starting at physical address 32 KB
 So:❓ How can a program that thinks it is running at address 0
actually run at physical address 32 KB without knowing it?
 This is exactly why address translation is required.
[Link], CSE 09-02-2026 36
 Physical memory is divided into
16 KB slots
 So the process’s virtual address
0 actually maps to physical
address 32 KB.
 The other two slots are free (16
KB-32 KB and 48 KB-64 KB)

[Link], CSE 09-02-2026 37


 Dynamic relocation is a hardware technique
 that allows the OS to load a process anywhere in physical
memory, while the process thinks it starts at address 0.
 This is done using two special CPU registers:
• Base register
• Bounds (Limit) register

[Link], CSE 09-02-2026 38


1. Base Register
• Stores the starting physical address of the process
• Used to translate virtual addresses to physical addresses

2. Bounds (Limit) Register


• Stores the size of the process address space
• Used to protect memory
• Ensures the process does not access memory outside its
area

[Link], CSE 09-02-2026 39


Key Idea
• Program is written as if it starts at address 0
• OS decides where to place it in physical memory
• Hardware automatically converts addresses at runtime

Address Translation Rule


Physical Address = Virtual Address + Base
 This conversion is done by the MMU

[Link], CSE 09-02-2026 40


Example
• Process size = 16 KB
• Base register = 32 KB
• Bounds register = 16 KB

Instruction Fetch Example


 Virtual instruction address = 128
 Physical Address = 32 KB (32768) + 128 = 32896

Note: 1 KB = 1024 bytes


[Link], CSE 09-02-2026 41
Data Access Example
 Virtual address of variable x = 15 KB
 Physical Address = 32 KB + 15 KB = 47 KB
 Process thinks it accessed 15 KB
 Hardware actually accesses 47 KB

[Link], CSE 09-02-2026 42


 Protection Using Bounds Register
 Before adding the base:
 CPU checks: 0 ≤ Virtual Address < Bounds
 If invalid:
 CPU raises an exception
 Process is terminated

Small Translation Example


Process size = 4 KB
Base address = 16 KB
[Link], CSE 09-02-2026 43
Imagine a process with an address space of size 4 KB
[Link], CSE 09-02-2026 44
 Segmentation is a memory management technique
 where a process is divided into logical parts, called segments.
 Each segment has:
• Its own base register (starting physical address)
• Its own bounds/limit register (size of the segment)
 This is why it is called generalized base and bounds.

[Link], CSE 09-02-2026 45


 A program is usually divided into:
• Code segment – program instructions
• Heap segment – dynamically allocated memory
• Stack segment – function calls and local variables
 Each segment is managed independently.

Why Segmentation Is Needed


• Avoids wasting memory
• Only used memory is placed in physical memory
• Different segments can be placed in different locations
• Provides better protection
[Link], CSE 09-02-2026 46
Example

[Link], CSE 09-02-2026 47


How Address Translation Works
Physical Address = Base + Offset
 Where:
Offset = Virtual Address − Start of Segment
 Before translation:
Offset < Segment Size (bounds check)

[Link], CSE 09-02-2026 48


Example 1:
Code Segment Access:
 Virtual address = 100
 Code segment starts at virtual address 0
 Base = 32 KB

 Offset: 100 − 0 = 100

 Physical address: 32 KB + 100 = 32868


 Valid memory access
[Link], CSE 09-02-2026 49
Example 2: Heap Segment Access
 Heap starts at virtual address 4 KB (4096)
 Virtual address = 4200
 Heap base = 34 KB (34816)

 Offset: 4200 − 4096 = 104

 Physical address: 34816 + 104 = 34920


 Correct address

[Link], CSE 09-02-2026 50


Example 3: Invalid Access (Segmentation Fault)
• Virtual address = 7 KB
• Heap size = 2 KB
• Heap valid range = 4 KB to 6 KB

 Address is outside the segment


➡ CPU raises an exception
➡ OS terminates the process
 This is called a segmentation fault.

[Link], CSE 09-02-2026 51


Why Sharing Is Needed?
 Many processes often run the same program (example: many users
running the same editor).
 If each process keeps its own copy of code, memory is wasted.
 Solution: Share the code segment among processes.

How Sharing Works in Segmentation?


 Segmentation already divides memory into:
• Code
• Heap
• Stack
 Now we add protection bits for each segment.
[Link], CSE 10-02-2026 52
Protection Bits (Hardware Support)
 Each segment has protection bits that specify what is allowed:
 Read (R)
 Write (W)
 Execute (X)
Examples:
 Code segment → Read + Execute (RX)
 Heap segment → Read + Write (RW)
 Stack segment → Read + Write (RW)

[Link], CSE 10-02-2026 53


How Code Sharing Is Achieved?
 Code segment is marked read-only
 Same physical code segment is mapped into multiple processes
 No process can modify it

 ✔ Saves memory
✔ Maintains protection
✔ Processes still think memory is private

[Link], CSE 10-02-2026 54


Two Processes Sharing Code
 Process A code segment → Base
= 32 KB
 Process B code segment → Base
= 32 KB
 Both point to the same physical
memory for code.
 If: Process A tries to write to
code → ❌ exception
 Process B tries to execute code
→ ✔ allowed
[Link], CSE 10-02-2026 55
 When a memory access occurs, the MMU checks:
 Is the address within bounds?
 Is the operation allowed (R/W/X)?
 If either fails:
➡ Hardware raises an exception
➡ OS handles the error (often terminates the process)

What Happens Without Protection Bits?


 One process could modify shared code
 Other processes would be affected
 System becomes unsafe
[Link], CSE 10-02-2026 56
 Paging divides memory into fixed-size blocks instead of variable-
size segments.
 Virtual memory → divided into pages
 Physical memory → divided into frames
 Page size = frame size

Example: Virtual Address Space


 Size = 64 bytes
 Page size = 16 bytes
 Number of pages = 4 (Pages 0–3)

[Link], CSE 10-02-2026 57


Physical Memory
 Divided into 8 frames
 OS places virtual pages into
free physical frames:

[Link], CSE 10-02-2026 58


 Os keeps a per-process data structure known as a page table.
 The major role of the page table is to store address
translations for each of the virtual pages of the address
space,
 thus letting us know where in physical memory they live.
 Example:
 VP 0 → PF 3
 VP 1 → PF 7
 VP 2 → PF 5
 VP 3 → PF 2

[Link], CSE 10-02-2026 59


 Virtual Address Format in Paging
 Given:
 Address space = 64 bytes → 6-bit address
 Page size = 16 bytes → 4-bit offset
 So virtual address is split as:| VPN (2 bits) | Offset (4 bits) |

 Va5 is the highest-order bit of the virtual address


 Va0 the lowest order bit.
 Because we know the page size (16 bytes)
[Link], CSE 10-02-2026 60
 we can further divide the virtual address as follows:

 When a process generates a virtual address,


 the OS and hardware must combine to translate it into a
meaningful physical address.

[Link], CSE 10-02-2026 61


Example
 Given instruction:
movl 21, %eax
Step 1: Convert to binary 21 = 010101
Step 2: Split into VPN and Offset
 VPN 01 Offset 0101
 VPN = 01 (1) Offset = 0101 (5)
Step 3: Page Table Lookup
 From page table: VP 1 → PF 7
 PF 7 in binary = 111
[Link], CSE 10-02-2026 62
Step 4: Form Physical Address
 Replace VPN with PFN, keep offset unchanged:
 PFN 111 Offset 0101
 Final physical address:1110101 = 117 (decimal) (See Fig.18.2)
 Correct memory location accessed

Why Offset Is Not Changed


 Offset represents the byte inside the page
 Same offset applies in both virtual page and physical frame

[Link], CSE 10-02-2026 63


 A page table is a data structure used by the OS to translate:
Virtual Address → Physical Address
 Each process has its own page table.
 Instead of dealing with the full address, it works with:
 VPN – Virtual Page Number
 PFN – Physical Frame Number
 Think of it like a lookup table:
 Index → Virtual Page Number (VPN)
 Value → Page Table Entry (PTE)

[Link], CSE 11-02-2026 64


Linear Page Table
 The simplest page table is a linear page table.
 It is just an array
 Each array entry is a Page Table Entry (PTE)
 OS uses the VPN as the index
Example
 PageTable[VPN] → PTE → PFN
 So, if VPN = 5
 OS looks at PageTable[5] to find where that page is in
physical memory.
[Link], CSE 10-02-2026 65
 Each PTE contains:
 The physical frame number (PFN)
 Some control bits (very important!)

1. Valid Bit
 Tells whether the page is valid or not
 If invalid → accessing it causes a trap (error) to OS
 Used to support sparse address space
 Unused pages are marked invalid
 Saves physical memory
[Link], CSE 10-02-2026 66
2. Protection Bits
 Control how a page can be used:
 Read (R) – Can read?
 Write (W) – Can write?
 Execute (X) – Can execute instructions?
 If a program violates this → trap to OS

3. Present Bit
 1 → Page is in physical memory
 0 → Page is on disk (swapped out)
 Used in virtual memory & swapping
[Link], CSE 10-02-2026 67
4. Dirty Bit
 Indicates whether the page was modified
 If dirty = 1 → must be written back to disk before replacement

5. Accessed / Reference Bit


 Set when page is used
 Helps OS decide which page to remove (page replacement)

[Link], CSE 10-02-2026 68


Example
 Virtual Page 0 → Physical Frame 3
 Virtual Page 1 → Physical Frame 7
 Virtual Page 2 → Physical Frame 5
 Virtual Page 3 → Physical Frame 2

VPN PFN
 Page Table looks like: 0 3
1 7
2 5
3 2

[Link], CSE 11-02-2026 69


 Figure 18.5 shows an example page table entry from the x86
architecture.

 Present bit (P); read/write bit (R/W), determines if writes are allowed to
this page ; user/supervisor bit (U/S), determines if user-mode processes
can access the page;
 a few bits (PWT, PCD, PAT, and G) that determine how hardware caching
works for these pages;
 Accessed bit (A) and a dirty bit (D); page frame number (PFN)
[Link], CSE 11-02-2026 70
 TLB is a small, fast cache inside the CPU that stores recent
virtual-to-physical address translations.
TLB = Cache for page table entries

Why do we need a TLB?


 Without a TLB:
 CPU generates a virtual address
 OS looks up the page table in memory
 Then accesses the actual data in memory
 This means two memory accesses for every instruction → slow

[Link], CSE 10-02-2026 71


With TLB (Much Faster )
 CPU generates a virtual address

 TLB is checked first


 If found → physical address obtained immediately
 If not found → page table in memory is used
 This saves time and memory accesses.

[Link], CSE 10-02-2026 72


TLB Hit
 Required translation is found in TLB
 Very fast
 Physical address generated directly

TLB Miss
 Translation not found in TLB
 Page table is accessed from main memory
 Entry may be added to TLB

[Link], CSE 10-02-2026 73


 It depends on the system architecture.
 There are two possible ways a TLB miss is handled:

1. Hardware-Managed TLB (e.g., x86)


 Handled by hardware (CPU)
 What happens?
 CPU checks the TLB
 TLB miss occurs
 Hardware automatically walks the page table
 Finds the page table entry (PTE)
 Loads it into the TLB
 Continues execution
 OS is not involved unless there is a page fault
[Link], CSE 10-02-2026 74
2. Software-Managed TLB (e.g., MIPS, SPARC)
 Handled by the Operating System
 What happens?
 CPU detects a TLB miss
 CPU raises a TLB miss exception
 Control transfers to the OS
 OS searches the page table
 OS updates the TLB
 Execution resumes
 Slower than hardware handling, but more flexible

[Link], CSE 10-02-2026 75


[Link], CSE 10-02-2026 76

You might also like