Problem description

Yesterday afternoon, my colleague was studying virtual address mapping in Linux (the classic book “Programmer self-training – Linking, loading, and Libraries”). When he saw chapter 6.4, he was puzzled by the value of virtual address in an executable ELF file.

For example, the following C code:

First compile the 32-bit executable (static linking is used to avoid some non-topic distractions) :

gcc -m32 -static test.c -o test
Copy the code

Compile the executable file: test in ELF format.

At this point, use the readelf tool to view the segments in the executable:

In the red rectangle above, why is the address of the second segment 0x080e_9f5c?

This article mainly analyzes the origin and development of this value according to the explanation in the book.

ELF file format

In Linux, there are four types of files in ELF format, including object files, executable files, dynamic link library files, and core dump files.

If you want to master the basic knowledge of Linux systems, it is inevitable to study the ELF format.

I summarized this article a long time ago, “The compiled, linked cornerstone of Linux systems -ELF files: Peel back its layers and Explore bytecode Granularity,” which summarized the internalization of ELF files in detail.

I don’t want to repeat it here, just remember two things:

  1. From the compiler’s point of view, ELF files are made up of many sections;

  2. From the program loader’s point of view, ELF files are made up of many segments.

In fact, there is no essential difference between them, but the linker organizes the same sections of different target files together to form a segment in the linking stage.

For the test executable you just compiled, its load view looks like this:

This file has 5 segments. The first 2 segments need to be loaded into memory. The first 2 segments need to be loaded into memory.

The green arrows reflect that the code snippet contains many sections; The yellow arrows reflect that the data segment also contains many sections.

Address translation and memory mapping

From the perspective of address translation:

The CPU in Linux uses a virtual address. When addressing the virtual address, the MMU address needs to be translated to obtain the actual physical address. Then, the virtual address can read instructions, or read or write data in the physical memory.

In modern operating systems, MMU address translation units are basically implemented through page tables:

Of course, some systems have a two-level conversion (page table, page table), and some have a three-level or four-level page table.

From a memory mapping perspective:

When an executable program is loaded into the system, the operating system reads the contents of each segment of the ELF file into physical memory, which is then mapped to the corresponding virtual address (VirtAddr) of that segment.

Suppose the code segment in an executable program is 1.2K bytes long and the data segment is 1.3K bytes long.

When the operating system reads them into memory, it needs two physical memory pages to store them separately (each physical page is 4K in length):

Although each physical memory page is 4K in size, the code and data segments actually only use the space at the beginning of each page.

When the CPU needs to read the instructions in the code segment in the physical memory, the virtual address is the address in the range of 0x0000_1000 ~ 0x0000_1000 + 1.2K. After the page table conversion, the MMU unit will get the physical address of the physical page storing the code segment.

The same is true for addressing data segments: When the CPU needs to read and write data from the data segment in the physical memory, the virtual address is in the range 0x0000_2000 to 0x0000_2000 + 1.3K.

After the MMU unit goes through the page table transformation, it gets the physical address of the physical page that holds the data segment.

It can be seen that under this arrangement, the virtual address of each segment is aligned in 4K(0x1000).

Things would be much simpler if operating systems were simply mapped like this.

If so, consider the virtual address arrangement in the test executable at the beginning of this article:

  1. The starting virtual address of the code segment arrangement is 0x0804_8000, which is 4K aligned;

  2. The end virtual address of the code segment should be 0x0804_8000 + 0xA0725 = 0x080E_8725;

  3. The starting address of the data segment can be arranged at the next 4K aligned boundary address after 0x080e_8725, i.e., 0x080E_9000.

But such address arrangement, serious waste of physical memory space!

The 1.2k-byte code segment plus 1.3K-byte data segment would have required only one physical page (4KB), but two physical pages (8KB) were consumed.

In order to reduce the waste of physical memory, The Linux operating system adopts some ingenious ways to reduce the waste of physical memory. That is, the code segment and data segment of the adjacent part of the file are read into the same physical memory page, and then mapped into the virtual address space twice, as detailed below.

Repeat memory mapping in Linux

Let’s look at the structure of the test file:

The code segment starts at 0x00000 in the file and has a length of 0xA0725.

The start position of the data segment is 0xA0F5c, and the length is 0x1024.

You can see that there is a blank interval between the two and the length is: 0xA0F5C-0xA0725 = 0x837(decimal: 2103 bytes).

Since the operating system reads the test file into physical memory, it starts from the address 0x00000 at the beginning of the file and stores it in a physical page in units of 4KB.

  1. 0x00000 ~ 0x00FFF of the code segment in the file is read into a physical page;

  2. 0x01000 ~ 0x01FFF of the code segment in the file is read into the physical page;

  3. Everything below is split and copied like this;

The test file is “sliced” in 4KB units from its starting location and copied to different physical memory pages, as shown below:

Note: The addresses of these physical pages are likely to be discrete.

Here’s the interesting thing: the 4KB space where the code segment borders the data segment, which starts at 0xA0000 and ends at 0xA0FFF, is copied to the top orange physical page in physical memory.

When GCC is executed, the linker places the virtual address of the code segment at 0x0804_8000:

That is, when the CPU (or program code) uses an address in the range 0x0804_8000 to 0x0804_7FFF, the address mapping will find the light green physical page in physical memory that corresponds to the first 4KB of the test executable.

Moreover, from the point of view of the virtual address, its address is continuous, corresponding to the continuous content of the test file, which is the essence of virtual address mapping.

Start the code snippet at address 0x0804_8000, as determined by the Linux operating system.

So consider: what is the starting virtual address of the 4K page corresponding to the instructions in the last part of the code snippet?

The orange part of the virtual address is already marked in the figure above: 0x080E_8000, calculated as follows:

Taking the starting address of the code segment 0x0804_8000, plus the length of the code segment in memory 0xA0725, the result is 0x080E_8725.

Once aligned in 4K (0x1000), the last virtual page should be 0x080E_8000.

That is, the range 0x080E_8000 to 0x080E_8724 in the virtual address corresponds to the last instruction (0x725 bytes) in the test file.

In addition, how to calculate the two red addresses in the test file structure: 0xA0000, 0xA1000?

The length of the code segment is 0xA0725, which is divided by 4K as a unit. In other words, divide 0xA0725 by 0x1000 exactly, and get the starting address 0xA0000 of 4KB.

Similarly, the next 4KB starting address is 0xA1000.

Copy the 4K data in the file (including: part of the code segment content + 0x837 byte void + part of the data segment content) to the orange physical page at the top of the physical memory in the figure above.

In the virtual address space, there is also a 0x837 byte void, as shown below:

Below the void are the instructions for the code snippet; Above the void is the data in the data segment.

The physical page now contains both code and data.

So the CPU in the search part of the code and data, must be able to find the line!

The code snippet is easier to understand: the first 0x725 bytes from the beginning of the physical page are valid, and from the point of view of the virtual address, the first 0x725 bytes from 0x080E_8000 are valid.

Therefore, the virtual addresses used for this part of the code are in the range 0x080E_8000 to 0x080E_8724.

What about data segments?

Here’s the thing: Linux also maps virtual address Spaces 0x080e_9000 to 0x080e_9FFF to the top orange physical page in the physical memory.

As follows:

In the physical page, the data segment content starts from the top of the 0x837 byte void, so the corresponding: virtual address 0x080E_9000 to 0x080e_9FFF space, the content above 0x837 byte is the data segment content.

So in the virtual address space, what is the starting address of this data segment?

Just calculate the offset from the starting address of the 4K page above the 0x837 byte void, and then add the starting address of the 4K page 0x080E_9000 to get the starting address of the data segment (virtual address).

Since the virtual address, physical address, and test file are all divided in 4K units, this offset is equal to: the offset between the start address of the data segment in the test file (0xA0F5C) and the start address of the page (0xA0000).

0xa0F5C-0xA0000 = 0xF5C.

That is, the start address of the 4K page, whose offset is 0xF5C, is the start of the data segment.

Therefore, for virtual addresses, starting from 0x080E_9000, the content after the offset iS 0xF5C is the content of the data segment. The address value is 0x080E_9000 + 0xF5C = 0x080e_9F5C, as shown in the following figure:

This address is exactly what the readelf tool reads: the starting address of the data segment loaded into the virtual address space, as shown below:

At this point, the question raised at the beginning of the article has been explained!

The space occupied by the segment is 0x01e48(MemSiz read by the readelf tool), and the end address of the segment is the virtual address:

0x080e_9F5C + 0x01e48 = 0x080e_bda4

As follows:

summary

In Linux, the operation of repeatedly mapping content belonging to different segments is a bit like shared memory.

However, after repeated mapping, the virtual address of each segment still needs to be modified to the legal address of that segment.

After this operation, the boundaries of each segment in the virtual address are distinct, but the mapping to the physical memory page may be the same.





Recommended reading

[1] Series of articles on Linux From Scratch

[2] C language pointer – from the underlying principles to fancy skills, with graphics and code to help you explain thoroughly

[3] The underlying debugging principle of GDB is so simple

[4] Is inline assembly terrible? Finish this article and end it!

Other series: featured articles, Application design, Internet of Things, C language.