Yığın, assembly dilinde nasıl çalışır?


84

Şu anda yığının nasıl çalıştığını anlamaya çalışıyorum, bu yüzden kendime bir montaj dili öğretmeye karar verdim , bu kitabı kullanıyorum:

http://savannah.nongnu.org/projects/pgubook/

Gas kullanıyorum ve geliştirmemi Linux Mint üzerinde yapıyorum .

Kafam biraz karıştı:

Bildiğim kadarıyla bir yığın sadece bir veri yapısıdır. Bu yüzden eğer derlemede kodluyorsam yığını kendim uygulamam gerektiğini varsaydım. Bununla birlikte, gibi komutlar olduğu için durum böyle görünmüyor

pushl
popl

Öyleyse, x86 mimarisi için derlemede kodlama yaparken ve Gas sözdizimini kullanırken: yığın yalnızca zaten uygulanmış bir veri yapısı mı? Yoksa gerçekten donanım düzeyinde mi uygulanıyor? Yoksa başka bir şey mi? Ayrıca, diğer yonga setleri için çoğu montaj dili yığın halihazırda uygulanmış olabilir mi?

Bunun biraz aptalca bir soru olduğunu biliyorum ama aslında bu beni oldukça karıştırdı.


2
Bu cevapların çoğu, diller tarafından kullanıldığı şekliyle yığın hakkında, özellikle de yığın üzerinde argümanlar iletmekten bahsediyorlar. Çoğu CPU'da bu sadece dillerin uygulanmasını kolaylaştırmak içindir - Eğer derlemeyi elle kodluyorsanız, genellikle parametreleri yazmaçlardaki işlevlere aktarırsınız (En azından CPU yığın işlemleri için optimize edilmeden önce, çünkü diller onu çok fazla kullanıyordu). Yığın çoğunlukla çağrıları / dönüşleri sırayla tutmak içindir, ayrıca bir kesme (CPU'nun durumunu kaydetmesi gerekir), kullanacağı yazmaçlarda mevcut değerleri zorlar ve geri dönmeden önce bunları açar.
Bill K

Yanıtlar:


82

Ben öncelikle bir arasında bocalama düşünüyorum program's stackve any old stack.

Yığın

Last In First Out sistemindeki bilgilerden oluşan soyut bir veri yapısıdır. İstifin üzerine rastgele nesneler koyarsınız ve sonra onları tekrar çıkarırsınız, tıpkı bir giriş / çıkış tepsisi gibi, en üst öğe her zaman çıkarılan öğedir ve her zaman en üste koyarsınız.

Bir Programlar Yığını

Bir yığın, yürütme sırasında kullanılan bir bellek bölümüdür, genellikle program başına statik bir boyuta sahiptir ve sıklıkla işlev parametrelerini saklamak için kullanılır. Bir işlevi çağırdığınızda parametreleri yığının üzerine itersiniz ve işlev ya yığına doğrudan hitap eder ya da değişkenleri yığından çıkarır.

Bir program yığını genellikle bir donanım değildir (ancak bellekte tutulur, bu yüzden böyle tartışılabilir), ancak Yığın geçerli bir alanına işaret eden Yığın İşaretçisi genellikle bir CPU kaydıdır. Bu, yığının adreslediği noktayı değiştirebileceğiniz için onu LIFO yığınından biraz daha esnek hale getirir.

Uğraştığınız şey olan Donanım Yığınının iyi bir tanımını verdiği için wikipedia makalesini okuyup anladığınızdan emin olmalısınız .

Yığını eski 16 bit yazmaçlar açısından açıklayan ancak yardımcı olabilecek ve özellikle yığın hakkında bir diğeri olan bu öğretici de var .

Nils Pipenbrinck'ten:

Bazı işlemcilerin yığına erişmek ve bunları işlemek için (push, pop, stack pointer, vb.) Tüm talimatları uygulamadığını, ancak x86'nın kullanım sıklığı nedeniyle bunu yaptığını unutmamak gerekir. Bu durumlarda, bir yığın istiyorsanız, onu kendiniz uygulamanız gerekir (bazı MIPS ve bazı ARM işlemcileri yığınlar olmadan oluşturulur).

Örneğin, MIP'lerde aşağıdaki gibi bir itme talimatı uygulanır:

addi $sp, $sp, -4  # Decrement stack pointer by 4  
sw   $t0, ($sp)   # Save $t0 to stack  

Pop talimatı şöyle görünür:

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  

2
Btw - x86'da bu özel yığın talimatları var çünkü yığından bir şeyler itmek ve çıkarmak o kadar sık ​​oluyor ki onlar için kısa bir işlem kodu kullanmak iyi bir fikirdi (daha az kod alanı). MIPS ve ARM gibi mimariler bunlara sahip değildir, bu nedenle yığını kendi başınıza uygulamanız gerekir.
Nils Pipenbrinck

4
Yeni işlemcinizin 8086 ile bir dereceye kadar ikili uyumlu olduğunu ve bunun ilk mikroişlemci olan 8008'in bir gelişimi olan 8080 ile kaynak uyumlu olduğunu unutmayın. Bu kararların bazıları çok eskilere dayanıyor.
David Thornley

4
ARM'de, yığını manipüle etmek için tek talimatlar vardır, bunlar çok açık değildir çünkü STMDB SP olarak adlandırılırlar! (PUSH için) ve LDMIA SP! (POP için).
Adam Goode

1
Tanrım bu cevabın +500'e ihtiyacı var ... Sonsuza kadar bunu iyi açıklayacak bir şey bulamadım. Şimdilik bunu + 1'lemek için yeni hesaplar oluşturmayı düşünüyor ...
Gabriel


36

( 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 stackdizi hücrelerinde saklayabilirsiniz ( snippet # 2.1 ):

stack[333] = 123;
stack[517] = 456;
stack[555] = stack[333] + stack[517];

Ancak hangi hücrelerin stackhalihazı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_CAPACITYve 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; // move pointer to unused cell, where we can store what we need
    stack[SP] = a * 3;
    return stack[SP];
}

ve nasıl adlandırıldığını görün ( snippet # 4.3 ):

// SP == 1000
someVar = triple_noLocals(11);
// now SP == 999, but we don't need the value at stack[999] anymore
// and we will move the stack index back, so we can reuse this cell later
SP += 1; // SP == 1000 again

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; // note that `pop` decreases stack's size
}

Push / pop için ortak kullanım modeli geçici olarak bazı değerleri kaydediyor. Diyelim ki, değişkende yararlı bir şeyimiz var myVarve bazı nedenlerden dolayı üzerine yazacak hesaplamalar yapmamız gerekiyor ( snippet 5.3 ):

int myVar = ...;
push(myVar); // SP == 999
myVar += 10;
... // do something with new value in myVar
pop(myVar); // restore original value, SP == 1000

6. Fonksiyon parametreleri

Şimdi stack kullanarak parametreleri geçirelim ( snippet # 6 ):

int triple_noL_noParams() { // `a` is at index 999, SP == 999
    SP -= 1; // SP == 998, stack[SP + 1] == a
    stack[SP] = stack[SP + 1] * 3;
    return stack[SP];
}

int main(){
    push(11); // SP == 999
    assert(triple(11) == triple_noL_noParams());
    SP += 2; // cleanup 1 local and 1 parameter
}

7. returnifade

AX kaydında değer döndürelim ( snippet # 7 ):

void triple_noL_noP_noReturn() { // `a` at 998, SP == 998
    SP -= 1; // SP == 997

    stack[SP] = stack[SP + 1] * 3;
    AX = stack[SP];

    SP += 1; // finally we can cleanup locals right in the function body, SP == 998
}

void main(){
    ... // some code
    push(AX); // save AX in case there is something useful there, SP == 999
    push(11); // SP == 998
    triple_noL_noP_noReturn();
    assert(triple(11) == AX);
    SP += 1; // cleanup param
             // locals were cleaned up in the function body, so we don't need to do it here
    pop(AX); // restore 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() { // `a` at 997, `b` at 998, old AX at 999, SP == 997
    SP -= 2; // SP == 995

    stack[SP + 1] = stack[SP + 2] * 3; 
    stack[SP]     = stack[SP + 3] * 3;
    AX = stack[SP + 1] - stack[SP];

    SP += 2; // cleanup locals, SP == 997
}

int main(){
    push(AX); // SP == 999
    push(22); // SP == 998
    push(11); // SP == 997
    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; // SP == 994
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() { // `a` at 997, `b` at 998, SP == 997
    push(BP);   // save old BP, SP == 996
    BP = SP;    // create anchor, stack[BP] == old value of BP, now BP == 996
    SP -= 2;    // SP == 994

    stack[BP - 1] = stack[BP + 1] * 3;
    stack[BP - 2] = stack[BP + 2] * 3;
    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP;    // cleanup locals, SP == 996
    pop(BP);    // SP == 997
}

İş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 myAlgosağ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_withAnchorbizim durumumuzda işlev), öyle ki arayan ( mainiş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 trippleC ++ yolunu çağırmak yerine aşağıdakileri yapın:

  1. tripplekodunu myAlgogövdenin başına kopyala
  2. de myAlgogiriş üzerinden atlamak trippleile 'in kodunugoto
  3. tripplekodunu 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_ADDRESSaşağıdaki makro)
  4. 1. satırın ( tripplefonksiyon) adresine atlayın ve sonuna kadar çalıştırın (3. ve 4. birlikte CALLmakrodur)
  5. sonunda tripple(yerelleri temizledikten sonra), yığının tepesinden dönüş adresini al ve oraya atla ( RETmakro)

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 ):

// pushes the address of the code at label's location on the stack
// NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int)
// NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
#define PUSH_ADDRESS(labelName) {               \
    void* tmpPointer;                           \
    __asm{ mov [tmpPointer], offset labelName } \
    push(reinterpret_cast<int>(tmpPointer));    \
}

// why we need indirection, read https://stackoverflow.com/a/13301627/264047
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)

// generates token (not a string) we will use as label name. 
// Example: LABEL_NAME(155) will generate token `lbl_155`
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)

#define CALL_IMPL(funcLabelName, callId)    \
    PUSH_ADDRESS(LABEL_NAME(callId));       \
    goto funcLabelName;                     \
    LABEL_NAME(callId) :

// saves return address on the stack and jumps to label `funcLabelName`
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)

// takes address at the top of stack and jump there
#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] == old BP, stack[BP + 1] == return address
    stack[BP - 1] = stack[BP + 2] * 3;
    AX = stack[BP - 1];

    SP = BP;     
    pop(BP);
    RET();

my_algo_start:
    push(BP);   // SP == 995
    BP = SP;    // BP == 995; stack[BP] == old BP, 
                // stack[BP + 1] == dummy return address, 
                // `a` at [BP + 2], `b` at [BP + 3]
    SP -= 2;    // SP == 993

    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; // cleanup locals, SP == 997
    pop(BP);
}

int main() {
    push(AX);
    push(22);
    push(11);
    push(7777); // dummy value, so that offsets inside function are like we've pushed return address
    myAlgo_asm();
    assert(myAlgo_withCalls(11, 22) == AX);
    SP += 1; // pop dummy "return address"
    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 intSP 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:


Bunu uzun zaman önce sordum, bu gerçekten harika bir derinlemesine cevap. Teşekkürler.
bplus

Cevabınızın ilk bölümünde neden kayıtlar için 16 bitlik isimler kullanıyorsunuz? Gerçek 16 bit kod hakkında konuşuyorsanız, [SP]16 bitlik adresleme modu geçerli değildir. Muhtemelen kullanmak en iyisi ESP. Ayrıca, SPbir olarak ilan intederseniz, onu her öğe için 1 değil 4 ile değiştirmelisiniz. (Eğer beyan ettiyseniz long *SP, o SP += 2zaman 2 * sizeof(int)2 eleman artıracak ve böylece 2 elemanı kaldıracaksınız. Ancak intSP ile, bu , 32'deki SP += 8gibi olmalıdır add esp, 8. -bit asm.
Peter Cordes

Büyüleyici! Montajı C kullanarak açıklamaya çalışmanın ilginç olduğunu düşünüyorum. Bunu daha önce görmemiştim. Düzgün. "Yerel değişken yok" u "Yerel değişkenler nasıl çalışır" veya yalnızca "Yerel değişkenler" olarak yeniden adlandırmayı önerebilirim.
Dave Dopson

@PeterCordes 16 bitlik adların (SP, BP) nedeni açıklıktır - SP kolayca "yığın işaretçisi" olarak tercüme edilir. Uygun 32 bit isimler kullanırsam ya 16/32/64 bit modları arasındaki farkı açıklamam ya da açıklanmadan bırakmam gerekir. Niyetim, yalnızca Java veya Python bilen birinin, kafasını çok fazla çizmeden postayı takip edebilmesiydi. Ve bence bellek adresleme sadece okuyucunun dikkatini dağıtacaktır. Ayrıca meraklılar için konuya wikibook bağlantısı ekledim ve yazının sonunda ESP hakkında birkaç kelime söyledim.
Alexander Malakhov

1
Bunu önlemek için yığın büyüdüğünde değişmeyen bir çapa indeksine ihtiyacımız var. İhtiyaç yanlış kelimedir; -fomit-frame-pointergcc ve clang'da yıllardır öntanımlıdır. Gerçek asm'a bakan kişilerin, EBP / RBP'nin genellikle çerçeve işaretçisi olarak kullanılmayacağını bilmeleri gerekir. "Geleneksel olarak, insanlar push / pop ile değişmeyen bir çapa istiyorlardı, ancak derleyiciler değişen ofsetleri takip edebilirler." Ardından, DWARF .eh_framemeta verileri veya Windows x86-64 meta verileri mevcut olduğunda varsayılan olarak kullanılmayan eski yöntem olduğunu söylemek için geri izleme hakkındaki bölümü güncelleyebilirsiniz .
Peter Cordes

7

Yığının donanımda uygulanıp uygulanmadığına gelince, bu Wikipedia makalesi yardımcı olabilir.

X86 gibi bazı işlemci aileleri, o anda yürütülen iş parçacığının yığınını işlemek için özel talimatlara sahiptir. PowerPC ve MIPS dahil diğer işlemci aileleri, açık yığın desteğine sahip değildir, bunun yerine kurallara ve işletim sisteminin Uygulama İkili Arabirimi'ne (ABI) yığın yönetimini devretmeye dayanır.

Bu makale ve bağlantı verdiği diğer makaleler, işlemcilerde yığın kullanımı hakkında fikir edinmek için yararlı olabilir.


4

Kavram

Önce her şeyi, onu icat eden kişi sizmişsiniz gibi düşünün. Bunun gibi:

Önce bir diziyi ve düşük seviyede nasıl uygulandığını düşünün -> temelde sadece bir dizi bitişik bellek konumu (birbirinin yanında olan bellek konumları). Artık kafanızda o zihinsel imaja sahip olduğunuza göre, bu hafıza konumlarından HERHANGİ BİRİNE erişebileceğinizi ve dizinizdeki verileri kaldırırken veya eklerken bunu kendi isteğinizle silebileceğinizi düşünün. Şimdi aynı diziyi düşünün, ancak herhangi bir konumu silme olasılığı yerine, dizinizdeki verileri kaldırırken veya eklerken yalnızca SON konumu sileceğinize karar verin. Şimdi bu dizideki verileri bu şekilde değiştirmek için yeni fikrinize LIFO adı verilir, bu da Son Giren İlk Çıkar anlamına gelir. Fikriniz çok iyi çünkü diziden bir şey çıkardığınızda bir sıralama algoritması kullanmak zorunda kalmadan o dizinin içeriğini takip etmeyi kolaylaştırıyor. Ayrıca, dizideki son nesnenin adresinin ne olduğunu her zaman bilmek için, onu takip etmek için Cpu'daki bir Register'ı ayırırsınız. Şimdi, kaydın izini sürmenin yolu, dizinize bir şey eklediğinizde veya eklediğinizde, diziden çıkardığınız veya eklediğiniz nesnelerin miktarına göre kütüğünüzdeki adresin değerini azaltır veya artırırsınız. kapladıkları adres alanı miktarı). Ayrıca, kaydı tutmayı kolaylaştırmak ve bunu mümkün kılmak için, bu kaydı azalttığınız veya artırdığınız miktarın nesne başına bir miktara (4 bellek konumu, yani 4 bayt gibi) sabitlendiğinden de emin olmak istersiniz. bu kaydı bazı döngü yapılarıyla kullanmak için, çünkü döngüler yineleme başına sabit artış kullanır (örn. dizinizde bir döngü ile döngü yapmak için, yazmacınızı her yinelemede 4'lük artırmak için döngüyü oluşturursunuz, dizinizin içinde farklı boyutlarda nesneler varsa bu mümkün olmaz). Son olarak, bu yeni veri yapısını bir "Yığın" olarak adlandırmayı seçersiniz, çünkü size bir restoranda her zaman o yığının üstüne bir tabak ekledikleri veya bir tabak ekledikleri bir tabak yığınını hatırlatır.

Hayata geçirme

Gördüğünüz gibi bir yığın, onu nasıl değiştireceğinize karar verdiğiniz bitişik bellek konumlarından başka bir şey değildir. Bu nedenle, yığını kontrol etmek için özel komutları ve kayıtları kullanmanıza bile gerek olmadığını görebilirsiniz. Bunu, temel mov, add ve sub komutları ile ve ESP ve EBP yerine genel amaçlı kayıtları kullanarak kendiniz uygulayabilirsiniz:

mov edx, 0FFFFFFFFh

; -> Bu, yığınınızın başlangıç ​​adresi, kodunuzdan ve verilerinizden en uzakta olacak, ayrıca daha önce açıkladığım yığındaki son nesnenin kaydını tutan kayıt görevi görecektir. Buna "yığın işaretçisi" diyorsunuz, bu nedenle, ESP'nin normalde kullanıldığı yer olarak EDX yazmacını seçersiniz.

alt edx, 4

mov [edx], dword ptr [someVar]

; -> bu iki talimat yığın işaretçinizi 4 bellek konumu azaltacak ve 4 baytı [someVar] bellek konumundan başlayarak şimdi EDX'in işaret ettiği bellek konumuna kopyalayacaktır, tıpkı bir PUSH komutunun ESP'yi düşürmesi gibi, sadece burada yaptınız manuel olarak ve siz EDX kullandınız. Dolayısıyla, PUSH komutu temelde bunu ESP ile yapan daha kısa bir işlem kodudur.

mov eax, dword ptr [edx]

edx ekle, 4

; -> ve burada tam tersini yapıyoruz, önce EDX'in işaret ettiği bellek konumundan başlayarak 4 baytı EAX yazmacına kopyalıyoruz (burada keyfi olarak seçilir, istediğimiz yere kopyalayabilirdik). Ve sonra yığın işaretçimizi EDX'i 4 bellek konumu artırıyoruz. POP talimatının yaptığı budur.

Şimdi, PUSH ve POP komutlarının ve ESP ve EBP kayıtlarının, yukarıdaki "yığın" veri yapısı kavramını yazmayı ve okumayı kolaylaştırmak için Intel tarafından eklendiğini görebilirsiniz. Hala PUSH ve POP komutlarına ve yığın manipülasyonu için ayrılmış yazmaçlara sahip olmayan bazı RISC (Azaltılmış Komut Seti) CPU'ları vardır ve bu CPU'lar için montaj programları yazarken, yığını kendi başınıza uygulamanız gerekir. ben sana gösterdim.


3

Soyut bir yığını ve donanım uygulanan yığını karıştırırsınız. İkincisi zaten uygulanmıştır.


3

Sanırım aradığınız ana cevap zaten ima edildi.

Bir x86 bilgisayarı açıldığında, yığın kurulmaz. Programcı, önyükleme sırasında açıkça ayarlamalıdır. Ancak, zaten bir işletim sistemindeyseniz, bununla ilgilenilmiştir. Aşağıda basit bir önyükleme programından bir kod örneği verilmiştir.

Önce veri ve yığın segmenti kayıtları ayarlanır ve ardından yığın işaretçisi bunun ötesinde 0x4000 olarak ayarlanır.


    movw    $BOOT_SEGMENT, %ax
    movw    %ax, %ds
    movw    %ax, %ss
    movw    $0x4000, %ax
    movw    %ax, %sp

Bu koddan sonra yığın kullanılabilir. Şimdi bunun birkaç farklı yolla yapılabileceğinden eminim, ancak bence bu fikri açıklamalı.


3

Yığın, programların ve işlevlerin belleği kullanmasının bir yoludur.

Yığın her zaman kafamı karıştırdı, bu yüzden bir örnek yaptım:

Yığın sarkıt gibi

( svg sürümü burada )

  • Bir itme "tavana yeni bir sarkıt ekler".
  • Bir pop "sarkıttan fırlıyor".

Umarım kafa karıştırmaktan daha faydalıdır.

SVG görüntüsünü kullanmaktan çekinmeyin (CC0 lisanslı).


1

Yığın zaten var, bu yüzden kodunuzu yazarken bunu varsayabilirsiniz. Yığın, fonksiyonların dönüş adreslerini, yerel değişkenleri ve fonksiyonlar arasında aktarılan değişkenleri içerir. BP, SP (Stack Pointer) gibi kullanabileceğiniz yerleşik yığın yazmaçları da vardır, dolayısıyla bahsettiğiniz yerleşik komutlar da vardır. Yığın zaten uygulanmamışsa, işlevler çalışamaz ve kod akışı çalışamaz.


1

Yığın, (burada x86 mimarisi varsayılarak) yığın segmentine işaret eden yığın işaretçisi aracılığıyla "uygulanır" . Yığın üzerinde bir şey her itildiğinde (pushl, call veya benzer bir yığın işlem kodu aracılığıyla), yığın işaretçisinin işaret ettiği adrese yazılır ve yığın işaretçisi azaltılır (yığın aşağı doğru büyür , yani daha küçük adresler) . Yığından bir şey çıkardığınızda (popl, ret), yığın işaretçisi artar ve değer yığından okunur.

Bir kullanıcı alanı uygulamasında, yığın, uygulamanız başladığında sizin için zaten ayarlanmıştır. Çekirdek uzay ortamında, önce yığın segmentini ve yığın işaretçisini ayarlamanız gerekir ...


1

Gaz birleştiricisini özel olarak görmedim, ancak genel olarak yığın, yığının tepesinin bulunduğu bellekteki konuma bir referans korunarak "uygulanır". Bellek konumu, farklı mimariler için farklı adlara sahip olan, ancak yığın işaretçisi kaydı olarak düşünülebilecek bir kayıtta saklanır.

Pop ve push komutları sizin için çoğu mimaride mikro komutlar üzerine inşa edilerek uygulanır. Ancak, bazı "Eğitim Mimarileri" bunları kendi kendinize uygulamanızı gerektirir. İşlevsel olarak, itme bir şekilde şu şekilde uygulanacaktır:

   load the address in the stack pointer register to a gen. purpose register x
   store data y at the location x
   increment stack pointer register by size of y

Ayrıca, bazı mimariler son kullanılan bellek adresini Yığın İşaretçisi olarak depolar. Bazıları bir sonraki mevcut adresi saklar.


1

Stack nedir? Yığın, bir tür veri yapısıdır - bir bilgisayarda bilgi depolamanın bir yolu. Bir yığına yeni bir nesne girildiğinde, daha önce girilen tüm nesnelerin üstüne yerleştirilir. Başka bir deyişle, yığın veri yapısı tıpkı bir kart yığını, kağıtlar, kredi kartı postaları veya aklınıza gelebilecek diğer gerçek dünya nesneleri gibidir. Bir yığından bir nesneyi kaldırırken, önce üstteki nesne kaldırılır. Bu yönteme LIFO (son giren, ilk çıkar) adı verilir.

"Yığın" terimi, bir ağ protokol yığını için de kısa olabilir. Ağ oluşturmada, bilgisayarlar arasındaki bağlantılar bir dizi küçük bağlantıyla yapılır. Bu bağlantılar veya katmanlar, aynı şekilde oluşturuldukları ve atıldıkları için yığın veri yapısı gibi davranırlar.


0

Yığının bir veri yapısı olduğu konusunda haklısınız. Çoğunlukla, birlikte çalıştığınız veri yapıları (yığınlar dahil) soyuttur ve bellekte bir temsil olarak bulunur.

Bu durumda üzerinde çalıştığınız yığın daha maddi bir varlığa sahiptir - doğrudan işlemcideki gerçek fiziksel kayıtlarla eşleşir. Veri yapısı olarak yığınlar, verilerin girildikleri sıranın tersine kaldırılmasını sağlayan FILO (ilk giren, son çıkan) yapılardır. Görsel için StackOverflow logosuna bakın! ;)

Komut yığınıyla çalışıyorsunuz . Bu, işlemciyi beslediğiniz gerçek talimatlar yığınıdır.


yanlış. bu bir 'komut yığını' değildir (böyle bir şey var mı?), bu basitçe Yığın yazmacı aracılığıyla erişilen bir bellektir. geçici depolama, prosedür parametreleri ve (en önemli) işlev çağrıları için iade adresi için kullanılır
Javier

0

Çağrı yığını, x86 komut seti ve işletim sistemi tarafından uygulanır.

İtme ve açma gibi talimatlar, yığın işaretçisini ayarlarken, işletim sistemi her iş parçacığı için yığın büyüdükçe bellek ayırmaya özen gösterir.

X86 yığınının yukarıdan aşağıya doğru "büyümesi", bu mimariyi arabellek taşması saldırısına daha duyarlı hale getirir.


1
Neden x86 yığınının küçülmesi onu arabellek taşmalarına karşı daha duyarlı hale getiriyor? Genişleyen bir segmentle aynı taşmayı elde edemez misiniz?
Nathan Fellman

@nathan: yalnızca uygulamanın yığına negatif miktarda bellek ayırmasını sağlayabilirseniz.
Javier

1
Arabellek taşması saldırıları, yığın tabanlı bir dizi - char userName [256] 'nin sonunu yazar, bu, belleği aşağıdan yukarıya doğru yazar, bu da dönüş adresi gibi şeylerin üzerine yazmanıza olanak tanır. Yığın aynı yönde büyüdüyse, yalnızca ayrılmamış yığının üzerine yazabilirsiniz.
Maurice Flanagan

0

Bir yığının 'sadece' bir veri yapısı olduğu konusunda haklısınız. Bununla birlikte, burada özel bir amaç için kullanılan, donanım uygulanmış bir yığını ifade eder - "Yığın".

Birçok kişi, (yazılım) yığın veri yapısına karşı donanım uygulanmış yığın hakkında yorum yaptı. Üç ana yığın yapısı türü olduğunu eklemek isterim -

  1. Bir çağrı yığını - Sorduğunuz şey hangisi! İşlev parametrelerini ve dönüş adresini vb. Saklar. Bu kitaptaki Bölüm 4 (4. sayfa hakkında her şey yani sayfa 53) işlevlerini okuyun. İyi bir açıklaması var.
  2. Programınızda özel bir şey yapmak için kullanabileceğiniz genel bir yığın ...
  3. Genel bir donanım yığını Bundan
    emin değilim, ancak bazı mimarilerde genel amaçlı donanım uygulanmış bir yığın olduğunu bir yerde okuduğumu hatırlıyorum. Bunun doğru olup olmadığını bilen biri varsa, lütfen yorum yapın.

Bilmeniz gereken ilk şey, programladığınız mimaridir, ki kitap bunu açıklar (biraz önce baktım - bağlantı). Bir şeyleri gerçekten anlamak için, x86'nın belleği, adresleme, yazmaçları ve mimarisi hakkında bilgi edinmenizi öneririm (kitaptan öğrendiğinizin bu olduğunu varsayıyorum).


0

LIFO tarzında yerel durumu kaydetmeyi ve geri yüklemeyi gerektiren işlev çağırma (genelleştirilmiş bir ortak rutin yaklaşımın aksine), montaj dillerinin ve CPU mimarilerinin temelde bu işlevselliği içinde oluşturduğu inanılmaz derecede yaygın bir ihtiyaç olarak ortaya çıkıyor. Muhtemelen iş parçacığı oluşturma, bellek koruması, güvenlik seviyeleri vb. kavramları için söylenebilir. Teoride kendi yığınınızı, çağrı kurallarını vb. uygulayabilirsiniz, ancak bazı işlem kodları ve mevcut çalışma zamanlarının çoğunun bu yerel "yığın" kavramına dayandığını varsayıyorum. .


0

stackhafızanın bir parçasıdır. onun için kullanmak inputve outputbir functions. ayrıca fonksiyonun dönüşünü hatırlamak için kullanılır.

esp register yığın adresini hatırlamaktır.

stackve espdonanım tarafından uygulanır. ayrıca kendiniz de uygulayabilirsiniz. programınızı çok yavaşlatacaktır.

misal:

nop // esp= 0012ffc4

0 // esp= 0012ffc0, Dword [0012ffc0] = 00000000 itin

proc01 // esp= 0012ffbc, Dword [0012ffbc] = eip, eip= adrr [proc01] çağrısı

pop eax// eax= Dword [ esp], esp= esp+ 4


0

Yığının işlev açısından nasıl çalıştığını araştırıyordum ve bu blogu harika buldum ve sıfırdan yığın kavramını ve yığındaki yığının değerini nasıl depoladığını gördüm.

Şimdi cevabınızda. Python ile açıklayacağım ama stack'in herhangi bir dilde nasıl çalıştığını iyi anlayacaksınız.

görüntü açıklamasını buraya girin

Bu bir program:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

görüntü açıklamasını buraya girin

görüntü açıklamasını buraya girin

Kaynak: Cryptroix

blogda ele aldığı bazı konular:

How Function work ?
Calling a Function
 Functions In a Stack
 What is Return Address
 Stack
Stack Frame
Call Stack
Frame Pointer (FP) or Base Pointer (BP)
Stack Pointer (SP)
Allocation stack and deallocation of stack
StackoverFlow
What is Heap?

Ama python dili ile açıklıyor, böylece isterseniz bir göz atabilirsiniz.


Criptoix sitesi öldü ve web.archive.org'da bir kopya yok
Alexander Malakhov

1
@AlexanderMalakhov Cryptroix, barındırma sorunu nedeniyle çalışmıyordu. Cryptroix şimdi çalışıyor ve çalışıyor.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.