最小堆疊分配

最小堆疊分配

我正在研究ELF規範(http://www.skyfree.org/linux/references/ELF_Format.pdf),關於程式載入過程,我不清楚的一點是堆疊是如何初始化的,以及初始頁面大小是多少。這是測試(在 Ubuntu x86-64 上):

$ cat test.s
.text
  .global _start
_start:
  mov $0x3c,%eax
  mov $0,%edi
  syscall
$ as test.s -o test.o && ld test.o
$ gdb a.out -q
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x400078
(gdb) run
Starting program: ~/a.out 

Breakpoint 1, 0x0000000000400078 in _start ()
(gdb) print $sp
$1 = (void *) 0x7fffffffdf00
(gdb) info proc map
process 20062
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 ~/a.out
      0x7ffff7ffa000     0x7ffff7ffd000     0x3000        0x0 [vvar]
      0x7ffff7ffd000     0x7ffff7fff000     0x2000        0x0 [vdso]
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

ELF 規範對於這個堆疊頁如何或為什麼存在一開始幾乎沒有說什麼,但我可以找到一些參考資料,說堆疊應該用指向 argc 的 SP 進行初始化,其中 argv、envp 和上面的輔助向量那個,我已經證實了這一點。但SP下面還有多少可用空間呢?在我的系統上,有0x1FF00映射到 SP 下面的字節,但大概這是從堆疊頂部開始倒數的,並且完整映射中0x7ffffffff000有字節。0x21000是什麼影響了這個數字?

我知道堆疊正下方的頁面是一個“保護頁面”,如果我寫入它,它會自動變得可寫並且“沿著堆疊向下增長”(大概這樣天真的堆疊處理“就可以工作”),但是如果我分配一個巨大的堆疊幀,那麼我可能會超出保護頁和段錯誤,所以我想確定在進程啟動時已經正確分配了多少空間。

編輯:更多的數據讓我更不確定發生了什麼事。測試如下:

.text
  .global _start
_start:
  subq $0x7fe000,%rsp
  movq $1,(%rsp)
  mov $0x3c,%eax
  mov $0,%edi
  syscall

我在這裡使用了不同的常數值0x7fe000來看看會發生什麼,對於這個值,是否出現段錯誤是不確定的。根據 GDB 的說法,subq指令本身會擴大 mmap 的大小,這對我來說很神秘(Linux 如何知道我的寄存器中有什麼?),但該程式通常會因某種原因在退出時使 GDB 崩潰。它不可能是 ASLR 導致不確定性,因為我沒有使用 GOT 或任何 PLT 部分;可執行檔每次總是載入到虛擬記憶體中的相同位置。那麼這是 PID 或實體記憶體的某種隨機性滲透嗎?總而言之,我很困惑到底有多少堆疊實際上可以合法地用於隨機訪問,以及在更改 RSP 或寫入「超出合法記憶體範圍」的區域時需要多少堆疊。

答案1

我不認為這個問題真的與 ELF 有關。據我所知,ELF定義了一種方法“平裝「將程式映像寫入文件,然後重新組裝以準備首次執行。如果作業系統行為尚未提升到 POSIX,那麼堆疊的定義及其實現方式介於 CPU 特定和作業系統特定之間。 儘管毫無疑問,ELF 規範對其堆疊上的需求提出了一些要求。

最小堆疊分配

從你的問題來看:

我知道堆疊正下方的頁面是一個“保護頁面”,如果我寫入它,它會自動變得可寫並且“沿著堆疊向下增長”(大概這樣天真的堆疊處理“就可以工作”),但是如果我分配一個巨大的堆疊幀,那麼我可能會超出保護頁和段錯誤,所以我想確定在進程啟動時已經正確分配了多少空間。

我正在努力尋找這方面的權威參考。但我發現足夠多的非權威參考資料表明這是不正確的。

據我所知,保護頁用於捕獲最大堆疊分配之外的訪問,而不是用於“正常”堆疊增長。實際的記憶體分配(將頁面對應到記憶體位址)是按需完成的。即:當存取記憶體中未映射的位址(位於 stack-base 和 stack-base - max-stack-size + 1 之間)時,CPU 可能會觸發異常,但核心會透過映射頁面來處理異常內存,不會級聯分段錯誤。

因此,存取最大分配範圍內的堆疊不應導致分段錯誤。正如你所發現的

最大堆疊分配

研究文件應該遵循有關線程創建和圖像加載的 Linux 文件(叉子(2),克隆(2),執行(2))。 execve 的文檔提到了一些有趣的事:

參數大小和環境的限制

...剪斷...

在核心 2.6.23 及更高版本上,大多數體系結構支援從軟派生的大小限制RLIMIT_STACK資源限制(參見取得限制(2)

...剪斷...

這證實了該限制需要架構來支援它,並且還引用了它的限制(取得限制(2))。

RLIMIT_STACK

這是進程堆疊的最大大小(以位元組為單位)。達到此限制後,會產生 SIGSEGV 訊號。要處理此訊號,進程必須使用備用訊號堆疊 (sigaltstack(2))。

從 Linux 2.6.23 開始,此限制也決定了進程命令列參數和環境變數所使用的空間量;有關詳細信息,請參閱 execve(2)。

透過更改 RSP 暫存器來增加堆疊

我不知道 x86 彙編程式。 但我會提請您注意“堆疊錯誤異常”,當 SS 暫存器更改時,x86 CPU 可能會觸發該異常。 如果我錯了請糾正我,但我相信在 x86-64 SS:SP 上剛剛變成“RSP”。因此,如果我理解正確的話,堆疊錯誤異常可以由遞減的 RSP ( ) 觸發subq $0x7fe000,%rsp

請參閱此處第 222 頁:https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce.html

答案2

每個進程記憶體區域(例如程式碼、靜態資料、堆疊、堆疊等)都有邊界,任何區域以外的記憶體存取或對唯讀區域的寫入存取都會產生 CPU 異常。核心維護這些記憶體區域。區域外的存取以分段錯誤訊號的形式傳播到使用者空間。

並非所有異常都是透過存取區域之外的記憶體產生的。區域內訪問也可能產生異常。例如,如果頁面未對應到實體內存,則頁面錯誤處理程序會以對正在運行的進程透明的方式處理此問題。

進程主堆疊區域最初只有少量的頁框映射到它,但是當透過堆疊指標將更多資料推送到它時,它會自動增長。異常處理程序檢查存取是否仍在為堆疊保留的區域內,如果是,則分配一個新的頁框。從使用者層級程式碼的角度來看,這是自動發生的。

保護頁放置在堆疊區域末端之後,以偵測堆疊區域的溢位。最近(2017 年)有人意識到單一保護頁是不夠的,因為程式可能會被欺騙而大量減少堆疊指針,這可能使堆疊指標指向允許寫入的其他某個區域。此問題的「解決方案」是用 1 MB 的保護區域取代 4 kB 的保護頁。看到這個綠網文章

應該注意的是,該漏洞並非完全容易被利用,例如,它要求用戶可以透過呼叫來控製程式分配的記憶體量alloca。健全的程式應該檢查傳遞給 的參數alloca,特別是如果它來自使用者輸入。

相關內容