<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Linux從頭學03:如何告訴 CPU,代碼段、數據段、棧段在內存中什么位置?

          共 3807字,需瀏覽 8分鐘

           ·

          2021-07-17 19:30


          • 幾個重要的段寄存器

          • Linux 2.6 中的線性地址區(qū)間

          • 一個“完整”的 8086 匯編程序


          前兩篇文章,我們一起學習了 8086 處理器中關于 CPU、內存的基本使用方式,重點對段寄存器和內存的尋址方式進行了介紹。

          可能有些小伙伴會對此不屑:現在都是多核的現代處理器,操作系統(tǒng)已經變得非常的強大,為何還去學習這些古董知識?

          前幾天看到下面這段話,可以來回答這個問題:

          “我們都希望學習最新的、使用的東西,但學習的過程是客觀的?!?/p>

          “任何合理的學習過程(盡可能排除走彎路、盲目探索、不成系統(tǒng))都是一個循序漸進的過程?!?/p>

          “我們必須先通過一個易于全面把握的事物,來學習和探索一般的規(guī)律和方法?!?/p>

          就拿學習 Linux 操作系統(tǒng)來說,作為一個長期的學習計劃,不太可能一上來就閱讀最新的 Linux 5.13 版本的代碼。

          更有可能是先學習 0.11 版本,理解了其中的一些原理、思想之后,再循序漸進的向高版本進行學習、探索。

          那么對于 《Linux 從頭學》這個系列的文章來說,我是希望自己能夠把學習路線再拉長一些,從更底層的硬件機制、驅動原理開始,由簡入繁,一步一步最終把 Linux 操作系統(tǒng)這個塊硬骨頭給啃下來。

          那么今天我們就繼續(xù) 8086 下的學習,來看看一個相對“完整”程序的基本結構。

          幾個重要的段寄存器

          x86 系統(tǒng)中,段尋址機制以及相關的寄存器是如此的重要,以至于我忍不住在這里,把幾個段寄存器再小結一下。

          代碼段:用來存放代碼,段的基地址放在寄存器 CS 中,指令指針寄存器 IP 用來表示下一條指令在段中的偏移地址;

          數據段:用來存放程序處理的數據,段的基地址存放在寄存器 DS 中。對數據段中的某個數據進行操作時,直接在匯編代碼中通過立即數或寄存器來指定偏移地址;

          棧段:本質上也是用來存放數據,只不過它的操作方式比較特殊而已:通過 PUSH 和 POP 指令來進行操作。段的基地址存放在寄存器 SS 中,棧頂單元的偏移地址存放在寄存器 IP 中。

          這里的段,本質上是我們把內存上的某一塊連續(xù)的存儲空間,專門存儲某一類的數據。

          我們之所以能夠這么做,是因為 CPU 通過以上幾個寄存器,讓我們這樣的“安排”稱為一種可能。

          一句話總結:CPU 將內存中的某個段的內容當做代碼,是因為 CS:IP 指向了那里;CPU 將某個段當做棧,是因為 CS:SP 指向了那里。

          在之前的一篇文章中,演示了 ELF 格式的可執(zhí)行文件中,具體包含了哪些段《Linux系統(tǒng)中編譯、鏈接的基石-ELF文件:扒開它的層層外衣,從字節(jié)碼的粒度來探索》:

          雖然這張圖中描述的段結構更復雜,但是從本質上來說,它與 8086 中描述的段結構是一樣的!

          Linux 2.6 中的線性地址區(qū)間

          在一個現代操作系統(tǒng)中,一個進程中使用的的地址空間,一般稱作虛擬地址(也稱作邏輯地址)。

          虛擬地址首先經過段轉換,得到線性地址;然后線性地址再經過分頁轉換,得到最終的物理地址。

          這里再啰嗦一下,很多書籍中隊內存地址的稱呼比較多,都是根據作者的習慣來稱呼。

          我是按照上圖的方式來理解的:  編譯器產生的地址叫做虛擬地址,也叫做邏輯地址,然后經過兩級轉換,得到最終的物理地址。

          Linux 2.6 代碼中,由于 Linux 把整個 4 GB 的地址空間當做一個“扁平”的結果來處理(段的基地址是 0x0000_0000,偏移地址的最大值是 4GB),因此虛擬地址(邏輯地址)在數值上等于線性地址。

          我們再結合上次給出的這張圖來理解:

          這張圖的意思是:在 Linux 2.6 中,用戶代碼段的開始地址是 0,最大范圍是 4 GB;用戶數據段的開始地址是 0,最大范圍也是 4 GB;內核的數據段和代碼段也是如此。

          為什么:虛擬地址(邏輯地址)在數值上等于線性地址?

          線性地址 = 段基址 + 虛擬地址(偏移量),因為段基址為 0 ,所以線性地址在數值上等于虛擬地址。

          Linux 之所以要這樣安排,是因為它不想過多的利用 x86 提供的段機制來進行內存地址的管理,而是想充分利用分頁機制來進行更加靈活的地址管理。

          還有一點需要提醒一下:

          在上述描述的文字中,我都會標明一個機制或者策略,它是由 x86 平臺提供的,還是由 Linux 操作系統(tǒng)提供的。

          對于分頁機制也是如此,x86 硬件提供了分頁機制,但是 Linuxx86 提供的這個分頁機制的基礎上,進行了擴展,以達到更加靈活的內存地址管理目的。

          因此,各位小伙伴在看一些書籍的時候,心中要有一個譜:當前描述內容的上下文環(huán)境是什么。

          當我們創(chuàng)建一個進程的時候,在內核中就會記錄這個進程所擁有的所有線性地址區(qū)間

          進程所擁有的所有線性地址區(qū)間是一個動態(tài)的過程,根據程序的需求隨時進行擴展或縮小。例如:把一個文件映射到內存,動態(tài)加載/卸載一個動態(tài)庫等等。

          我們知道,內核在操作物理內存的時候,是通過“頁框”這個單位來管理的。

          一個頁框可以包含 1-n 個頁,每一頁的大小一般是 4 KB,這是對物理內存的管理。

          一個線性地址區(qū)間可以包含多個物理頁。每一個線性地址最終通過多級的頁表轉換,來最終得到一個物理地址。

          注意:上圖中,線性地址區(qū)間1,映射到物理地址空間中的 NPage,這些 Page 有可能是連續(xù)的,也有可能不是連續(xù)的。

          雖然在物理內存中是不連續(xù)的,但是由于被分頁轉換機制進行了屏蔽,我們在應用程序中都是按照連續(xù)的空間來使用的。

          一個“完整”的 8086 匯編程序

          我們再繼續(xù)回到 8086 系統(tǒng)中來。

          這里描述的地址,經過段地址轉換之后,就是一個物理地址,沒有經過復雜的頁表轉換。

          這也是我們以 8086 系統(tǒng)作為學習平臺的目的:拋開復雜的操作系統(tǒng),直接探索底層的東西。

          在這個最簡單的匯編程序中,會使用到 3 個段:代碼段,數據段和棧段。

          前面已經說到:所謂的段,就是一個地址空間。既然是一個地址空間,必然包含 2 個元素:從什么地方開始,長度是多少

          還是直接上代碼:

          assume ds:addr1, ss:addr2, cs:addr3

          addr1 segment ; 把數據段安排在這個位置
          db 32 dup (0) ; 這 32 個字節(jié),是數據段的大小
          addr1 end

          addr2 segment ; 把棧段安排在這個位置
          db 32 dup(0) ; 這 32 個字節(jié),是棧段的大小
          addr2 end

          addr3 segment ; 把代碼段安排在這個位置
          start
          mov ax, addr1
          mov ds, ax ; 設置數據段寄存器

          mov ax, addr2
          mov ss, ax ; 設置棧段寄存器
          mov sp, 20h ; 設置棧頂指針寄存器

          ... ; 其他代碼
          addr3 ends

          end start

          以上就是一個匯編代碼的基本程序結構,我們給它安排了 3 個段。

          3 個標號:addr1、addr2addr3,代表了每一個段的開始地址。在代碼段的開始部分,把數據段標號 addr1 代表的地址,賦值給 DS 寄存器;把棧段標號 addr2 代表的地址,賦值給 SS 寄存器。

          這里的標號,是不是與 C 語言中的 goto 標號很類似?都是表示一個地址。

          注意這里賦值給棧頂指針 SP 寄存器的值是 20H。

          因為棧段的使用是從高地址向低地址方向進行的,所以需要把棧頂指針設置為最大地址單元的下一個地址空間。

          假設把第一個數據入棧時(eg: 先執(zhí)行 mov ax, 1234h,再執(zhí)行 push ax),CPU 要做的事情是: 先執(zhí)行 SP = SP - 2,此時 SS:SP 指向 1000:001E,然后再把 1234h 存儲到這個地址空間:

          另外,代碼中最后一句 end start,用來告訴編譯器:代碼段中 start 標號代表的地址,就是這個程序的入口地址,編譯之后這個入口地址信息也會被寫入可執(zhí)行程序中。

          當可執(zhí)行文件被加載到內存中之后,加載程序會找到這個入口地址,然后把 CS:IP 設置為指向這個入口地址,從而開始執(zhí)行第一條指令。

          我們再來對比一下《Linux系統(tǒng)中編譯、鏈接的基石-ELF文件:扒開它的層層外衣,從字節(jié)碼的粒度來探索》中列出的 ELF 可執(zhí)行文件中的入口地址,它與上面 8086 下的 start 標號代表的入口地址,在本質上都是一樣的道理:


          ------ End ------

          推薦閱讀

          【1】C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹
          【2】一步步分析-如何用C實現面向對象編程
          【3】原來gdb的底層調試原理這么簡單
          【4】內聯匯編很可怕嗎?看完這篇文章,終結它!

          其他系列專輯精選文章C語言、Linux操作系統(tǒng)應用程序設計、物聯網


          星標公眾號,能更快找到我!


          瀏覽 45
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线观看免费视频a | 奇米久久778 | 韩国成人电影一区二区三区 | 91aV影院在线播放 | 国产人人干 |