我們可以寫一段簡單的c代碼(code/memory/segment_1.c):
#include <stdio.h>
int main()
{
int a = 1;
printf("Hello, World!");
return 0;
}
然后將其轉為匯編,運行:
gcc -S segment_1.c
之后會生成一個.s 文件(code/memory/segment_1.s),不用細看內容。
我們發現,這個匯編代碼中,有 .string main .text,反正分為不同的塊。
.file "segment_1.c"
.text
.section .rodata
.LC0:
.string "Hello, World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $1, -4(%rbp)
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
進程在內存中,主要是按照這種形式進行存儲的。
為什么要分段呢?
我們看上面那張圖片,分為不同段,每個段的讀寫屬性是不同的。比如正文段存儲代碼,就像我們上面的.s文件中的main部分。它在內存中是始終不變的,我們CPU就是從main中去取指執行。所以,它是只讀的。
而其他部分,也許是可讀可寫的。
通過分段,可以對不同的屬性代碼、數據進行更方便的管理。如果是打亂的放在內存中,那么讀寫屬性就很難控制。
所以通過分段機制,我們可以更好得控制不同段的屬性,這有利於內存的組織安排。更多的好處,可以后面慢慢體會。
分段機制與間接尋址
因為現在引入了分段機制,所以如果我們想要訪問某個地方的數據,指令,我們會引入間接尋址。
比如說,程序現在運行在代碼段(CS),然后呢,他想要訪問數據段的某個數據(DS),那么就可以使用 DS的基地址+偏移量,就可以訪問到數據段中的數據了。
就像我們下圖一樣,如果我們運行到某一條指令,他需要 DS:1 的數據,那么我們可以通過數據段開始的地址0x100 加上偏移量1,就可以直接訪問到 int a = 1 了。
所以,我們只需要知道不同段的開始地址加上偏移量,就可以訪問不同段的內容。
分段機制與LDT表
我們前面說到,因為分段機制,所以,如果我們想要去訪問某個段中的內容,我們需要獲得每個段的基址。所以,我們需要一個結構去儲存每個段的基址。這就是 LDT (本地描述表/local descriptor table) 表。
(名字,有點。。。,至少本地兩字說明,這個表是局部的,對於每個進程,都有一個LDT表)。
LDT 表
現在來說說 LDT 表,之前說了分段的好處,那么我們應該怎么設計 LDT 表呢?
我們需要用一些位來表示:可讀/可寫狀態,不同段的基址以及段的長度。
現在,我們可以通過每個進程的 LDT 表找到每個段所在的位置。
GDT 表
那么我們如何找到每個進程的 LDT 表呢?
這個,就輪到 GDT表了,與之前的 LDT 表,就差一個字母。G的意思,就是 global 全局的意思。
那意味着這張表,所有的進程都可以看到,進行訪問。
在 GDT 表中,有着不同 LDT 表的地址。這里涉及到 LDTR寄存器,這個寄存器被賦值為 GDT表中的一個 LDT 表的位置,然后CPU 可以通過這個寄存器,找到對應的進程的 LDT表。
可以看下下圖。
關於更多的 GDT 和 LDT 的介紹,可以看最后的參考部分。
分段機制與多進程
我們知道,CPU會采用某種策略,運行多個進程。這個時候,我們就需要保證不同的進程的內容,會存在不同的內存中,不能相互干擾。
這就要求,我們之前說的,每個進程都有一個 LDT表。可以將不同的進程內容映射到不同的內存地址。
總結
最后,就拿這張圖來總結下吧,大家可以用這張圖來回顧下前面的內容。
參考
《深入理解計算機系統》
- 7.9 加載可執行目標文件
《Linux 內核0.11完全注釋》
- 4.3 分頁機制(進階)
操作系統(哈工大李治軍老師)32講(全)超清 P20 L20 內存使用與分段