2017-09-16 83 views
6

如何分割棧工作?這個問題也適用於Boost.Coroutine,所以我在這裏也使用C++標記。主要的疑問來源於此article看起來他們做的是保持一定的空間,在堆棧的底部,並檢查它是否已經得到了通過註冊某種信號處理與分配存在的內存崩潰(可能通過mmapmprotect?)然後,當他們發現他們的空間不足時,他們繼續分配更多的內存,然後從那裏繼續。關於這個的3個問題如何分割棧工作

  1. 這不是構建用戶空間的東西嗎?他們如何控制新堆棧的分配位置,以及如何編譯程序以瞭解這些情況?

    推送指令基本上只是向堆棧指針添加一個值,然後將值存儲在堆棧的一個寄存器中,那麼推送指令如何知道新堆棧的啓動位置,相應地,彈出如何知道何時必須將堆棧指針移回舊棧?

  2. 他們也說

    我們已經有了一個新的堆棧段後,我們通過重試重新啓動goroutine導致我們用完堆棧

    的這是什麼意思的功能?他們重新啓動整個goroutine嗎?這不可能導致非確定性行爲嗎?

  3. 他們如何檢測程序已經溢出堆棧?如果他們將一個金絲雀存儲區保留在底部,那麼當用戶程序創建的數組足夠大而溢出時會發生什麼?這會不會導致堆棧溢出並且是潛在的安全漏洞?

如果實現是不同的圍棋和Boost我會很高興知道如何要麼他們應對這種情況

+2

開始使用版本1.3中的連續堆棧 – tkausl

+0

@tkausl嗯..只是瞭解它,這絕對看起來像是需要語言本身支持的東西,並且很難像Boost.Coroutine那樣實現庫。對?連續堆棧方法似乎更容易理解,儘管 – Curious

+0

@Curious:是需要語言支持--Go具有與C/C++不同的堆棧佈局和調用約定。 – JimB

回答

5

我給你一個可能實現的速寫。

首先,假設大多數堆棧幀比一些尺寸更小。對於更大的那些,我們可以在入口處使用更長的指令序列以確保有足夠的堆棧空間。假設我們在一個擁有4k頁的體系結構上,並且我們選擇4k-1作爲由快速路徑處理的最大大小的堆棧幀。

堆棧分配在底部的單一防護頁面。也就是說,沒有被映射爲寫入的頁面。在函數入口,堆棧指針由堆棧幀的大小,這是小於頁的尺寸,然後該程序安排寫在新分配的堆棧幀的最低地址的值遞減。如果到達堆棧的末端,則該寫入將導致處理器異常並最終轉變爲從操作系統到用戶程序的某種上調 - 例如, UNIX系列操作系統中的一個信號。

信號處理程序(或等價物)必須能夠確定這是來自故障指令地址和寫入地址的堆棧擴展故障。這是可以確定的,因爲指令位於函數的序言中,並且正在寫入的地址位於當前線程的堆棧的守護頁中。在序言中的指令可以通過在函數開始時要求非常特定的指令模式來識別,或者可能通過維護關於函數的元數據來識別。(可能使用回溯表)

此時處理程序可以分配一個新的堆棧塊,將堆棧指針設置爲塊的頂部,做一些處理解除堆棧塊的鏈接,然後調用發生故障的函數再次。第二次調用是安全的,因爲錯誤在編譯器生成的函數prolog中,並且在驗證有足夠的堆棧空間之前不允許有任何副作用。 (該代碼可能還需要修復將其自動推入堆棧的體系結構的返回地址,如果返回地址在寄存器中,則只需在第二次調用時位於同一個寄存器中。)

可能最簡單的處理解除鏈接的方法是將一個小的堆棧幀推送到新的擴展塊上,以執行返回解除新堆棧塊並釋放分配內存的例程。然後它將處理器寄存器返回到調用時導致堆棧需要擴展的狀態。

這種設計的優點是功能輸入序列非常少,並且在非擴展情況下速度非常快。缺點是,在堆棧確實需要擴展的情況下,處理器會引發異常,這可能比函數調用花費更多。

如果我理解正確,Go實際上並不使用警衛頁面。相反,函數prolog明確地檢查了堆棧限制,如果新的堆棧幀不適合,它調用一個函數來擴展堆棧。

Go 1.3將其設計更改爲不使用堆棧塊的鏈接列表。這是爲了避免陷阱成本,如果擴展邊界以某種調用模式多次在兩個方向上交叉。他們從一個小堆棧開始,並使用類似的機制來檢測擴展需求。但是,當發生堆棧擴展故障時,整個堆棧將移動到更大的塊。這消除了完全解除鏈接的需要。

這裏有不少細節。 (例如,可能無法在信號處理程序本身中執行堆棧擴展,而是處理程序可以安排暫停該線程並將其交給管理線程,可能必須使用專用信號堆棧來處理信號)

這種事情的另一個常見模式是運行時需要在當前棧幀下面有一定量的有效棧空間,用於類似信號處理程序或調用運行時特殊例程。 Go以這種方式工作,並且堆棧限制測試保證在當前幀下面有一定數量的堆棧空間可用。人們可以例如調用堆棧上的純C函數,只要保證它們不會消耗超過固定堆棧保留量。在棧幀(人們可以用它來調用C庫函數理論,雖然大多數的這些有他們可能多少堆棧使用沒有正式的規範。)

動態分配,如ALLOCA或堆棧分配的變長數組,爲實施增加了一些複雜性。如果例程可以在序言中計算幀的整個最終大小,那麼它是相當簡單的。在例程運行時,幀大小的任何增加都可能必須建模爲新的調用,儘管Go的新架構允許移動堆棧,但例程中的alloca點可以做成使得所有狀態都允許一個堆棧移動發生在那裏。