( Oynamak istemeniz durumunda, bu cevaptaki tüm kodların bir özünü yaptım )
2003'teki CS101 kursumda asm'de yalnızca en temel şeyleri yaptım. Asm ve stack'in nasıl çalıştığını gerçekten "anlamamıştım" ta ki bunun temelde C veya C ++ 'da programlama gibi olduğunu fark edene kadar ... ancak yerel değişkenler, parametreler ve işlevler olmadan. Muhtemelen henüz kulağa kolay gelmiyor :) Size göstereyim ( Intel sözdizimine sahip x86 asm için ).
1. Yığın nedir
Yığın, genellikle başlamadan önce her iş parçacığı için ayrılan bitişik bir bellek öbeğidir. Orada ne istersen saklayabilirsin. C ++ terimleriyle ( kod pasajı # 1 ):
const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];
2. Yığının üstü ve altı
Prensip olarak, değerleri rastgele stack
dizi hücrelerinde saklayabilirsiniz ( snippet # 2.1 ):
stack[333] = 123;
stack[517] = 456;
stack[555] = stack[333] + stack[517];
Ancak hangi hücrelerin stack
halihazırda kullanımda olduğunu ve hangilerinin "özgür" olduğunu hatırlamanın ne kadar zor olacağını bir düşünün . Bu nedenle yeni değerleri yan yana yığında saklıyoruz.
(X86) asm yığınıyla ilgili tuhaf bir şey, oraya son dizinden başlayarak bir şeyler eklemeniz ve daha düşük dizinlere geçmenizdir: yığın [999], sonra yığın [998] ve benzeri ( snippet # 2.2 ):
stack[999] = 123;
stack[998] = 456;
stack[997] = stack[999] + stack[998];
Ve hala için "resmi" ad (Dikkat, artık karıştırılmamalıdır olacak) stack[999]
olan yığının alt .
Son kullanılan hücreye ( stack[997]
yukarıdaki örnekte) yığının üstü denir (bkz . Yığının tepesinin x86'da olduğu yer ).
3. Yığın işaretçisi (SP)
Bu tartışmanın amacı için CPU kayıtlarının global değişkenler olarak temsil edildiğini varsayalım (bkz. Genel Amaçlı Kayıtlar ).
int AX, BX, SP, BP, ...;
int main(){...}
Yığının tepesini izleyen özel bir CPU kaydı (SP) vardır. SP bir göstericidir (0xAAAABBCC gibi bir hafıza adresini tutar). Ancak bu yazının amaçları doğrultusunda, onu bir dizi dizini (0, 1, 2, ...) olarak kullanacağım.
Bir iş parçacığı başladığında SP == STACK_CAPACITY
ve ardından program ve işletim sistemi gerektiğinde onu değiştirir. Kural, yığının tepesinin ötesinde yığın hücrelere yazamazsınız ve SP'den daha az olan herhangi bir indeks geçersiz ve güvensizdir ( sistem kesintileri nedeniyle ), bu nedenle
önce SP'yi azaltırsınız ve sonra yeni tahsis edilen hücreye bir değer yazarsınız.
Yığındaki birkaç değeri arka arkaya itmek istediğinizde, tümü için önceden yer ayırabilirsiniz ( pasaj # 3 ):
SP -= 3;
stack[999] = 12;
stack[998] = 34;
stack[997] = stack[999] + stack[998];
Not. Şimdi yığın üzerindeki ayırmanın neden bu kadar hızlı olduğunu görebilirsiniz - bu sadece tek bir kayıt azalmasıdır.
4. Yerel değişkenler
Bu basit işleve ( pasaj # 4.1 ) bir göz atalım :
int triple(int a) {
int result = a * 3;
return result;
}
ve yerel değişken kullanmadan yeniden yazın ( snippet # 4.2 ):
int triple_noLocals(int a) {
SP -= 1;
stack[SP] = a * 3;
return stack[SP];
}
ve nasıl adlandırıldığını görün ( snippet # 4.3 ):
someVar = triple_noLocals(11);
SP += 1;
5. Bas / çıkar
Yığının üstüne yeni bir elemanın eklenmesi o kadar sık bir işlemdir ki, CPU'ların bunun için özel bir talimatı vardır push
. Bunu şu şekilde uygulayacağız ( snippet 5.1 ):
void push(int value) {
--SP;
stack[SP] = value;
}
Aynı şekilde, yığının en üst öğesini alarak ( pasaj 5.2 ):
void pop(int& result) {
result = stack[SP];
++SP;
}
Push / pop için ortak kullanım modeli geçici olarak bazı değerleri kaydediyor. Diyelim ki, değişkende yararlı bir şeyimiz var myVar
ve bazı nedenlerden dolayı üzerine yazacak hesaplamalar yapmamız gerekiyor ( snippet 5.3 ):
int myVar = ...;
push(myVar);
myVar += 10;
...
pop(myVar);
6. Fonksiyon parametreleri
Şimdi stack kullanarak parametreleri geçirelim ( snippet # 6 ):
int triple_noL_noParams() {
SP -= 1;
stack[SP] = stack[SP + 1] * 3;
return stack[SP];
}
int main(){
push(11);
assert(triple(11) == triple_noL_noParams());
SP += 2;
}
7. return
ifade
AX kaydında değer döndürelim ( snippet # 7 ):
void triple_noL_noP_noReturn() {
SP -= 1;
stack[SP] = stack[SP + 1] * 3;
AX = stack[SP];
SP += 1;
}
void main(){
...
push(AX);
push(11);
triple_noL_noP_noReturn();
assert(triple(11) == AX);
SP += 1;
pop(AX);
...
}
8. Yığın taban işaretçisi (BP) ( çerçeve işaretçisi olarak da bilinir ) ve yığın çerçevesi
Daha "gelişmiş" işlevi alalım ve asm benzeri C ++ 'da ( snippet # 8.1 ) yeniden yazalım :
int myAlgo(int a, int b) {
int t1 = a * 3;
int t2 = b * 3;
return t1 - t2;
}
void myAlgo_noLPR() {
SP -= 2;
stack[SP + 1] = stack[SP + 2] * 3;
stack[SP] = stack[SP + 3] * 3;
AX = stack[SP + 1] - stack[SP];
SP += 2;
}
int main(){
push(AX);
push(22);
push(11);
myAlgo_noLPR();
assert(myAlgo(11, 22) == AX);
SP += 2;
pop(AX);
}
Şimdi, tripple
(snippet # 4.1) ' de yaptığımız gibi, geri dönmeden önce sonucu orada saklamak için yeni yerel değişken sunmaya karar verdiğimizi hayal edin . İşlevin gövdesi ( snippet # 8.2 ) olacaktır:
SP -= 3;
stack[SP + 2] = stack[SP + 3] * 3;
stack[SP + 1] = stack[SP + 4] * 3;
stack[SP] = stack[SP + 2] - stack[SP + 1];
AX = stack[SP];
SP += 3;
Gördüğünüz gibi, fonksiyon parametrelerine ve yerel değişkenlere yapılan her referansı güncellememiz gerekiyordu. Bundan kaçınmak için, yığın büyüdüğünde değişmeyen bir çapa indeksine ihtiyacımız var.
Mevcut top'u (SP'nin değerini) BP yazmacına kaydederek, fonksiyon girişi üzerine (yereller için yer ayırmadan önce) çapayı oluşturacağız. Snippet # 8.3 :
void myAlgo_noLPR_withAnchor() {
push(BP);
BP = SP;
SP -= 2;
stack[BP - 1] = stack[BP + 1] * 3;
stack[BP - 2] = stack[BP + 2] * 3;
AX = stack[BP - 1] - stack[BP - 2];
SP = BP;
pop(BP);
}
İşleve ait olan ve işleve tam olarak hakim olan yığın dilimine işlevin yığın çerçevesi denir . Örneğin myAlgo_noLPR_withAnchor
, yığın çerçevesi stack[996 .. 994]
(her ikisi de dahil).
Çerçeve, fonksiyonun BP'sinde başlar (fonksiyonun içinde güncelledikten sonra) ve bir sonraki yığın çerçevesine kadar sürer. Dolayısıyla, yığındaki parametreler, arayanın yığın çerçevesinin parçasıdır (bkz. Not 8a).
Notlar:
8a. Wikipedia, parametreler hakkında aksini söylüyor , ancak burada Intel yazılım geliştiricisinin kılavuzuna bağlıyım, bkz. Cilt. 1, bölüm 6.2.4.1 Yığın Çerçevesi Taban İşaretçisi ve Şekil 6-2 bölüm 6.3.2 Uzak ÇAĞRI ve RET İşlemi . İşlevin parametreleri ve yığın çerçevesi, işlevin etkinleştirme kaydının bir parçasıdır (bkz . İşlev tehlikeleri üzerindeki gen ).
8b. BP noktasından fonksiyon parametrelerine pozitif ofsetler ve negatif ofsetler yerel değişkenlere işaret eder. Bu,
8c'de hata ayıklamak için oldukça kullanışlıdır . stack[BP]
önceki yığın çerçevesinin adresini saklar,stack[stack[BP]]
önceki yığın çerçevesini depolar vb. Bu zinciri takip ederek, programdaki tüm işlevlerin henüz geri dönmeyen karelerini keşfedebilirsiniz. Hata ayıklayıcılar size yığın
8d'yi nasıl çağırdığınızı gösterir . myAlgo_noLPR_withAnchor
Çerçeveyi kurduğumuz ilk 3 talimat (eski BP'yi kaydedin, BP'yi güncelleyin, yereller için yer ayırın) işlev prologu olarak adlandırılır
9. Çağrı kuralları
Snippet 8.1'de için parametreleri myAlgo
sağdan sola aktardık ve sonucunu döndürdük AX
. Paramları soldan sağa geçip geri dönebiliriz BX
. Veya BX ve CX'te parametreleri geçin ve AX'te geri dönün. Açıkçası, arayan ( main()
) ve çağrılan işlev, tüm bu şeylerin nerede ve hangi sırayla saklandığına karar vermelidir.
Çağrı kuralı , parametrelerin nasıl geçirildiğine ve sonucun nasıl döndürüldüğüne ilişkin bir dizi kuraldır.
Yukarıdaki kodda cdecl çağrı kuralı kullandık :
- Parametreler, çağrı sırasında yığındaki en düşük adreste ilk bağımsız değişken olacak şekilde yığın üzerinde geçirilir (en son <...> itilir). Arayan, aramadan sonra parametreleri yığından geri almaktan sorumludur.
- dönüş değeri AX'e yerleştirilir
- EBP ve ESP, aranan uç tarafından korunmalıdır (
myAlgo_noLPR_withAnchor
bizim durumumuzda işlev), öyle ki arayan ( main
işlev) bir arama ile değiştirilmemiş kayıtlara güvenebilir.
- Diğer tüm kayıtlar (EAX, <...>), aranan uç tarafından serbestçe değiştirilebilir; Arayan, işlev çağrısından önce ve sonra bir değeri korumak isterse, değeri başka bir yere kaydetmelidir (bunu AX ile yaparız)
(Kaynak: Stack Overflow Documentation'dan örnek "32-bit cdecl"; telif hakkı 2016, icktoofay ve Peter Cordes ; CC BY-SA 3.0 altında lisanslanmıştır. Tam Stack Overflow Documentation içeriğinin bir arşivi archive.org'da bulunabilir, burada bu örnek konu kimliği 3261 ve örnek kimliği 11196 ile indekslenmiştir.)
10. İşlev çağrıları
Şimdi en ilginç kısım. Veriler gibi, çalıştırılabilir kod da bellekte depolanır (yığın için bellekle tamamen ilgisizdir) ve her komutun bir adresi vardır.
Aksi belirtilmediği zaman CPU, komutları bellekte saklandıkları sıraya göre birbiri ardına yürütür. Ancak CPU'ya bellekteki başka bir konuma "atlamasını" ve oradan talimatları yürütmesini sağlayabiliriz. Asm'de herhangi bir adres olabilir ve C ++ gibi daha yüksek seviyeli dillerde yalnızca etiketlerle işaretlenmiş adreslere atlayabilirsiniz ( geçici çözümler vardır, ancak en azından söylemek gerekirse bunlar hoş değildir).
Bu işlevi alalım ( snippet # 10.1 ):
int myAlgo_withCalls(int a, int b) {
int t1 = triple(a);
int t2 = triple(b);
return t1 - t2;
}
Ve tripple
C ++ yolunu çağırmak yerine aşağıdakileri yapın:
tripple
kodunu myAlgo
gövdenin başına kopyala
- de
myAlgo
giriş üzerinden atlamak tripple
ile 'in kodunugoto
tripple
kodunu yürütmemiz gerektiğinde , tripple
çağrıdan hemen sonra kod satırının yığın adresini kaydedin , böylece buraya daha sonra dönebilir ve yürütmeye devam edebiliriz ( PUSH_ADDRESS
aşağıdaki makro)
- 1. satırın (
tripple
fonksiyon) adresine atlayın ve sonuna kadar çalıştırın (3. ve 4. birlikte CALL
makrodur)
- sonunda
tripple
(yerelleri temizledikten sonra), yığının tepesinden dönüş adresini al ve oraya atla ( RET
makro)
C ++ 'da belirli bir kod adresine atlamanın kolay bir yolu olmadığından, atlama yerlerini işaretlemek için etiketleri kullanacağız. Aşağıdaki makroların nasıl çalıştığını ayrıntılarına girmeyeceğim, sadece söylediklerimi yaptıklarına inanın ( snippet # 10.2 ):
#define PUSH_ADDRESS(labelName) { \
void* tmpPointer; \
__asm{ mov [tmpPointer], offset labelName } \
push(reinterpret_cast<int>(tmpPointer)); \
}
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)
#define CALL_IMPL(funcLabelName, callId) \
PUSH_ADDRESS(LABEL_NAME(callId)); \
goto funcLabelName; \
LABEL_NAME(callId) :
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)
#define RET() { \
int tmpInt; \
pop(tmpInt); \
void* tmpPointer = reinterpret_cast<void*>(tmpInt); \
__asm{ jmp tmpPointer } \
}
void myAlgo_asm() {
goto my_algo_start;
triple_label:
push(BP);
BP = SP;
SP -= 1;
stack[BP - 1] = stack[BP + 2] * 3;
AX = stack[BP - 1];
SP = BP;
pop(BP);
RET();
my_algo_start:
push(BP);
BP = SP;
SP -= 2;
push(AX);
push(stack[BP + 2]);
CALL(triple_label);
stack[BP - 1] = AX;
SP -= 1;
pop(AX);
push(AX);
push(stack[BP + 3]);
CALL(triple_label);
stack[BP - 2] = AX;
SP -= 1;
pop(AX);
AX = stack[BP - 1] - stack[BP - 2];
SP = BP;
pop(BP);
}
int main() {
push(AX);
push(22);
push(11);
push(7777);
myAlgo_asm();
assert(myAlgo_withCalls(11, 22) == AX);
SP += 1;
SP += 2;
pop(AX);
}
Notlar:
10a. dönüş adresi yığın üzerinde saklandığından, prensipte onu değiştirebiliriz. Bu nasıl yığın çökertilmesi saldırı çalışır
10b. triple_label
öğesinin "sonundaki" son 3 talimata (yerelleri temizleme, eski BP'yi geri yükleme, geri dönme) işlevin son sözü denir
11. Montaj
Şimdi gerçek asm için bakalım myAlgo_withCalls
. Bunu Visual Studio'da yapmak için:
- derleme platformunu x86 olarak ayarlayın ( x86_64 değil )
- derleme türü: Hata Ayıklama
- myAlgo_withCalls içinde bir yerde kesme noktası ayarlayın
- çalıştırın ve yürütme kesme noktasında durduğunda Ctrl + Alt + D tuşlarına basın
Asm benzeri C ++ ile bir fark, asm yığınının ints yerine baytlar üzerinde çalışmasıdır. Bu nedenle, biri için yer ayırmak için int
SP 4 bayt azaltılır.
Burada (go # 11.1 snippet'ine yorumlardaki hat numaraları vardır özünden ):
; 114: int myAlgo_withCalls(int a, int b) {
push ebp ; create stack frame
mov ebp,esp
; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12)
sub esp,0D8h ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal
push ebx ; cdecl requires to save all these registers
push esi
push edi
; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
; see https://stackoverflow.com/q/3818856/264047
; I guess that's for ease of debugging, so that stack is filled with recognizable values
; 0CCCCCCCCh in binary is 110011001100...
lea edi,[ebp-0D8h]
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
; 115: int t1 = triple(a);
mov eax,dword ptr [ebp+8] ; push parameter `a` on the stack
push eax
call triple (01A13E8h)
add esp,4 ; clean up param
mov dword ptr [ebp-8],eax ; copy result from eax to `t1`
; 116: int t2 = triple(b);
mov eax,dword ptr [ebp+0Ch] ; push `b` (0Ch == 12)
push eax
call triple (01A13E8h)
add esp,4
mov dword ptr [ebp-14h],eax ; t2 = eax
mov eax,dword ptr [ebp-8] ; calculate and store result in eax
sub eax,dword ptr [ebp-14h]
pop edi ; restore registers
pop esi
pop ebx
add esp,0D8h ; check we didn't mess up esp or ebp. this is only for debug builds
cmp ebp,esp
call __RTC_CheckEsp (01A116Dh)
mov esp,ebp ; destroy frame
pop ebp
ret
Ve tripple
( pasaj # 11.2 ) için asm :
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0CCh]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
imul eax,dword ptr [ebp+8],3
mov dword ptr [ebp-8],eax
mov eax,dword ptr [ebp-8]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
Umarım bu yazıyı okuduktan sonra, meclis eskisi kadar şifreli görünmüyor :)
İşte gönderinin gövdesinden bağlantılar ve daha fazla okuma:
- Eli Bendersky , yığının tepesinin x86'da olduğu yer - üst / alt, itme / açma , SP, yığın çerçevesi, arama kuralları
- Eli Bendersky , x86-64'te yığın çerçeve düzeni - x64'ten geçen değiştirgeler, yığın çerçevesi, kırmızı bölge
- Mariland Üniversitesi , Yığını Anlamak - kavramları yığmak için gerçekten iyi yazılmış bir giriş. (MIPS için (x86 değil) ve GAS sözdiziminde, ancak bu konu için önemsiz). İlgileniyorsanız MIPS ISA Programlama hakkındaki diğer notlara bakın .
- x86 Asm wikibook, Genel Amaçlı Kayıtlar
- x86 Sökme wikibook, The Stack
- x86 Sökme wikibook, İşlevler ve Yığın Çerçeveleri
- Intel yazılım geliştiricisinin kılavuzları - Gerçekten sağlam olmasını bekliyordum, ancak şaşırtıcı bir şekilde okunması oldukça kolay (bilgi miktarı çok fazla olsa da)
- Jonathan de Boyne Pollard, Fonksiyon tehlikelerine ilişkin gen - prolog / epilog, yığın çerçeve / aktivasyon kaydı, kırmızı bölge