Çağrı yığını tam olarak nasıl çalışır?


103

Programlama dillerinin düşük seviyeli işlemlerinin nasıl çalıştığını ve özellikle OS / CPU ile nasıl etkileşime girdiklerini daha derinlemesine anlamaya çalışıyorum. Muhtemelen burada Stack Overflow'da her yığın / yığınla ilgili başlıktaki her cevabı okudum ve hepsi harika. Ancak henüz tam olarak anlamadığım bir şey var.

Bu işlevi, geçerli Rust kodu olma eğiliminde olan sözde kodda düşünün ;-)

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(a, b);
    doAnotherThing(c, d);
}

Yığının X satırındaki gibi görüneceğini varsayıyorum:

Stack

a +-------------+
  | 1           | 
b +-------------+     
  | 2           |  
c +-------------+
  | 3           | 
d +-------------+     
  | 4           | 
  +-------------+ 

Şimdi, yığının nasıl çalıştığı hakkında okuduğum her şey, LIFO kurallarına kesinlikle uyduğudur (son giren, ilk çıkar). Tıpkı .NET, Java veya başka bir programlama dilindeki bir yığın veri türü gibi.

Ama durum buysa, X satırından sonra ne olur? Açıkçası Çünkü sonraki şey biz ihtiyaç çalışmak için ave bancak bu işletim sistemi / işlemci (?) Dışarı pop sahip anlamına gelir dve cilk geri almak için ave b. Ama sonra ayağından vururdu, çünkü ihtiyacı cve bir dsonraki çizgide.

Öyleyse, perde arkasında tam olarak ne olduğunu merak ediyorum.

Bir başka ilgili soru. Bunun gibi diğer işlevlerden birine bir referans verdiğimizi düşünün:

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(&a, &b);
    doAnotherThing(c, d);
}

Bir şeyleri nasıl anladığıma göre, bu, içindeki parametrelerin doSomethingesasen ave biçindeki gibi aynı bellek adresine işaret ettiği anlamına gelir foo. Ama orada o zaman yine bu araçlar o biz gidene kadar yığını açılır aveb oluyor.

Bu iki durum , yığının tam olarak nasıl çalıştığını ve LIFO kurallarına tam olarak nasıl uyduğunu tam olarak anlamadığımı düşündürüyor .


14
LIFO, yalnızca yığın üzerinde yer ayırmak için önemlidir. Diğer birçok değişkenin altında olsa bile, en azından yığın çerçevenizde bulunan herhangi bir değişkene (işlevin içinde bildirilen) her zaman erişebilirsiniz
VoidStar

2
Başka bir deyişle, LIFOöğeleri yalnızca yığının sonuna ekleyip kaldırabileceğiniz ve her zaman herhangi bir öğeyi okuyabileceğiniz / değiştirebileceğiniz anlamına gelir.
HolyBlackCat

12
Neden basit bir işlevi -O0 ile derledikten sonra parçalarına ayırıp oluşturulan talimatlara bakmıyorsunuz? Güzel, iyi, öğretici ;-). Kodun, RAM'in R bölümünü iyi bir şekilde kullandığını göreceksiniz; adreslere doğrudan istediği zaman erişir. Bir değişken adını bir adres kaydına (yığın işaretçisi) göre bir uzaklık olarak düşünebilirsiniz. Diğerlerinin dediği gibi, yığın, istifleme açısından sadece LIFO'dur (özyineleme için iyi vb.). Erişim açısından LIFO değil. Erişim tamamen rastgele.
Peter - Monica'yı eski

6
Bir dizi kullanarak kendi yığın veri yapınızı oluşturabilirsiniz ve yalnızca en üstteki öğenin dizinini depolayabilir, ittiğinizde artırabilir, açtığınızda azaltabilirsiniz. Bunu yaptıysanız, dizilerde her zaman yaptığınız gibi, dizideki herhangi bir öğeye herhangi bir zamanda basmadan veya basmadan erişebilirsiniz. Yaklaşık olarak aynı şey burada da oluyor.
Crowman

3
Temel olarak, yığın / öbek adlandırma talihsizdir. Veri yapılarının terminolojisini istiflemek ve yığmak için çok az benzerlik gösterirler, bu yüzden onları aynı şekilde adlandırmak çok kafa karıştırıcıdır.
Siyuan Ren

Yanıtlar:


117

Çağrı yığını aynı zamanda çerçeve yığını olarak da adlandırılabilir. LIFO ilkesinden sonra yığınlanan
şeyler yerel değişkenler değil, çağrılan işlevlerin tüm yığın çerçeveleridir ("çağrılar") . Yerel değişkenler , sırasıyla, sözde işlev önsözü ve sonsözde bu çerçevelerle birlikte itilir ve çıkarılır .

Çerçevenin içinde değişkenlerin sırası tamamen belirtilmemiştir; Derleyiciler , hizalamalarını optimize etmek için çerçeve içindeki yerel değişkenlerin konumlarını uygun şekilde "yeniden sıralar", böylece işlemci onları olabildiğince çabuk getirebilir. Önemli olan, değişkenlerin bazı sabit adreslere göre ofsetinin çerçevenin ömrü boyunca sabit olmasıdır - bu nedenle, bir çapa adresi, örneğin çerçevenin kendisinin adresini almak ve bu adresin ofsetleriyle çalışmak yeterlidir. değişkenler. Böyle bir çapa adresi aslında sözde taban veya çerçeve işaretçisinde bulunurEBP kaydında saklanır. Öte yandan ofsetler, derleme zamanında açıkça bilinir ve bu nedenle makine koduna kodlanır.

Wikipedia'dan alınan bu grafik , tipik çağrı yığınının 1 gibi yapılandırıldığını gösterir :

Yığının resmi

Çerçeve işaretçisinde bulunan adrese erişmek istediğimiz değişkenin ofsetini ekleyin ve değişkenimizin adresini alıyoruz. Kısaca söylemek gerekirse, kod onlara doğrudan temel işaretçiden sabit derleme zamanı uzaklıkları aracılığıyla erişir; Basit işaretçi aritmetiği.

Misal

#include <iostream>

int main()
{
    char c = std::cin.get();
    std::cout << c;
}

gcc.godbolt.org bize

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp

    movl    std::cin, %edi
    call    std::basic_istream<char, std::char_traits<char> >::get()
    movb    %al, -1(%rbp)
    movsbl  -1(%rbp), %eax
    movl    %eax, %esi
    movl    std::cout, %edi
    call    [... the insertion operator for char, long thing... ]

    movl    $0, %eax
    leave
    ret

.. için main. Kodu üç alt bölüme ayırdım. İşlev prologu ilk üç işlemden oluşur:

  • Temel işaretçi yığının üzerine itilir.
  • Yığın işaretçisi temel işaretçiye kaydedilir
  • Yığın işaretçisi yerel değişkenlere yer açmak için çıkarılır.

Daha sonra cinEDI kaydı 2'ye taşınır ve getçağrılır; Dönüş değeri EAX'tedir.

Çok uzak çok iyi. Şimdi ilginç olan şey oluyor:

EAX'in 8 bitlik kayıt AL tarafından belirlenen düşük sıralı baytı alınır ve temel işaretçinin hemen ardından baytta saklanır : Yani, -1(%rbp)temel işaretçinin ofseti olur -1. Bu bayt bizim değişkenimizdirc . Ofset negatiftir çünkü yığın x86'da aşağı doğru büyür. Bir sonraki işlem depolar ceax'a: EAX ESI taşınır coutile EDI taşınır ve sonra ekleme operatör olarak adlandırılır coutve cbağımsız değişkenler olarak.

En sonunda,

  • Dönüş değeri mainEAX: 0'da saklanır. Bunun nedeni örtük returnifadedir. Bunun xorl rax raxyerine de görebilirsiniz movl.
  • ayrılın ve çağrı sitesine geri dönün. leavebu sonsözü kısaltıyor ve dolaylı olarak
    • Yığın işaretçisini temel işaretçiyle değiştirir ve
    • Temel işaretçiyi açar.

Bu işlemden sonra ve retgerçekleştirildikten sonra, çerçeve etkin bir şekilde açıldı, ancak arayanın cdecl arama kuralını kullanırken hala argümanları temizlemesi gerekiyor. Stdcall gibi diğer kurallar, aranan ucun, örneğin bayt miktarını 'a aktararak toparlanmasını gerektirir ret.

Çerçeve İşaretçisi Eksikliği

Temel / çerçeve işaretçisinden değil, bunun yerine yığın işaretçisinden (ESB) ofsetleri kullanmak da mümkündür. Bu, aksi takdirde çerçeve işaretçisi değerini içerecek olan EBP kaydını keyfi kullanım için kullanılabilir hale getirir - ancak bazı makinelerde hata ayıklamayı imkansız hale getirebilir ve bazı işlevler için dolaylı olarak kapatılacaktır . Özellikle x86 dahil olmak üzere yalnızca birkaç yazmaçlı işlemciler için derleme yaparken kullanışlıdır.

Bu optimizasyon FPO (çerçeve işaretçisi ihmali) olarak bilinir ve -fomit-frame-pointerGCC ve -OyClang'da tarafından ayarlanır ; her optimizasyon seviyesi> 0 tarafından dolaylı olarak tetiklendiğine dikkat edin, ancak ve ancak hata ayıklama hala mümkünse, bunun dışında herhangi bir maliyeti yoktur. Daha fazla bilgi için buraya ve buraya bakın .


1 Yorumlarda belirtildiği gibi, çerçeve işaretçisinin muhtemelen dönüş adresinden sonraki adresi göstermesi amaçlanmıştır.

2 R ile başlayan yazmaçların, E ile başlayanların 64-bit karşılıkları olduğuna dikkat edin. EAX, RAX'in dört düşük sıralı baytını belirtir. Netlik için 32 bitlik yazmaçların adlarını kullandım.


1
Mükemmel cevap. Verileri ofsetlerle ele almak benim için eksik
Christoph

1
Çizimde küçük bir hata olduğunu düşünüyorum. Çerçeve işaretçisinin, dönüş adresinin diğer tarafında olması gerekir. Bir işlevden ayrılma genellikle şu şekilde yapılır: Yığın işaretçisini çerçeve işaretçisine taşıyın, yığından arayanların çerçeve işaretçisini
kaldırın

kasperd kesinlikle haklı. Çerçeve işaretçisini ya hiç kullanmazsınız (geçerli optimizasyon ve özellikle x86 gibi kayıttan yoksun mimariler için son derece yararlıdır) ya da kullanırsınız ve bir öncekini yığın üzerinde depolarsınız - genellikle dönüş adresinden hemen sonra. Çerçevenin nasıl kurulup kaldırılacağı büyük ölçüde mimariye ve ABI'ye bağlıdır. Her şeyin .. daha ilginç (ve değişken boyutlu argüman listeleri gibi şeyler var!)
Olduğu

3
@Christoph Bence buna kavramsal bir bakış açısıyla yaklaşıyorsunuz. İşte bunu umarız bir şekilde açıklığa kavuşturacak bir yorum - RTS veya RunTime Stack, diğer yığınlardan biraz farklıdır, çünkü bu "kirli yığın" dır - aslında sizin olmayan bir değere bakmanızı engelleyen hiçbir şey yoktur. t üstte. Mavi yöntem için gerekli olan yeşil yöntem için diyagramda "Dönüş Adresi" olduğuna dikkat edin! parametrelerden sonra. Mavi yöntem, önceki kare açıldıktan sonra dönüş değerini nasıl alır? Şey, bu kirli bir yığın, yani içeri girip yakalayabilir.
Riking

1
Çerçeve işaretçisi aslında gerekli değildir çünkü bunun yerine yığın işaretçisinden ofsetler her zaman kullanılabilir. Varsayılan olarak x64 mimarilerini hedefleyen GCC, yığın işaretçisi kullanır ve rbpdiğer işleri yapmak için serbest kalır.
Siyuan Ren

27

Çünkü açıkçası, ihtiyacımız olan bir sonraki şey a ve b ile çalışmak ama bu OS / CPU'nun (?) A ve b'ye geri dönmek için önce d ve c'yi açması gerektiği anlamına gelir. Ama sonra kendisini ayağından vurur çünkü bir sonraki satırda c ve d'ye ihtiyacı vardır.

Kısacası:

Argümanları açmaya gerek yok. Çağıran tarafından fooişleve doSomethingve yerel değişkenlere iletilen argümanların doSomething tümü, temel işaretçiden bir uzaklık olarak referans alınabilir .
Yani,

  • Bir işlev çağrısı yapıldığında, işlevin argümanları yığın üzerinde BASTIRILIR. Bu argümanlara ayrıca temel işaretçi ile başvurulur.
  • İşlev çağırıcısına döndüğünde, dönen işlevin argümanları LIFO yöntemi kullanılarak yığından POP'lanır.

Detayda:

Kural, her işlev çağrısının bir yığın çerçevesinin oluşturulmasıyla sonuçlanmasıdır (minimum dönülecek adres). Bu nedenle, funcAaramalar funcBve funcBaramalar durumunda funcC, üç yığın çerçevesi birbiri üzerine kurulur. Bir işlev geri döndüğünde çerçevesi geçersiz hale gelir . İyi davranan bir işlev yalnızca kendi yığın çerçevesi üzerinde hareket eder ve diğerininkini ihlal etmez. Başka bir deyişle, POP işlemi üstteki yığın çerçevesine gerçekleştirilir (işlevden dönerken).

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

Sorunuzdaki yığın, arayan tarafından oluşturulmuştur foo. Ne zaman doSomethingve doAnotherThingçağrıldıklarında, kendi yığınlarını kurarlar. Şekil, bunu anlamanıza yardımcı olabilir:

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

, Dikkat ediniz (fonksiyon vücut işlevi vücut yığın yana hareket ettirmek zorunda olacaktır, geri dönüş adresi saklanır konumdan (daha yüksek adresleri) aşağı hareket ettirmek zorunda olacak ve lokal değişkenler ulaşmak için, daha düşük adresleri argümanlar ulaşmak için ) iade adresinin saklandığı konuma göre. Aslında, işlev için derleyici tarafından üretilen tipik kod tam olarak bunu yapacaktır. Derleyici bunun için EBP adında bir kayıt ayırır (Base Pointer). Aynısının başka bir adı da çerçeve işaretçisidir. Derleyici tipik olarak, işlev gövdesi için ilk şey olarak, geçerli EBP değerini yığına iter ve EBP'yi geçerli ESP'ye ayarlar. Bu, bir kez yapıldığında, işlev kodunun herhangi bir bölümünde, bağımsız değişken 1'in EBP + 8 uzakta olduğu (her arayanın EBP'si ve dönüş adresi için 4 bayt), bağımsız değişken 2'nin EBP + 12 (ondalık) uzakta olduğu anlamına gelir, yerel değişkenler EBP-4n uzakta.

.
.
.
[ebp - 4]  (1st local variable)
[ebp]      (old ebp value)
[ebp + 4]  (return address)
[ebp + 8]  (1st argument)
[ebp + 12] (2nd argument)
[ebp + 16] (3rd function argument) 

Fonksiyonun yığın çerçevesinin oluşturulması için aşağıdaki C koduna bir göz atın:

void MyFunction(int x, int y, int z)
{
     int a, int b, int c;
     ...
}

Arayan onu aradığında

MyFunction(10, 5, 2);  

aşağıdaki kod üretilecek

^
| call _MyFunction  ; Equivalent to: 
|                   ; push eip + 2
|                   ; jmp _MyFunction
| push 2            ; Push first argument  
| push 5            ; Push second argument  
| push 10           ; Push third argument  

ve işlevin derleme kodu (dönmeden önce aranan uç tarafından ayarlanır)

^
| _MyFunction:
|  sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
|  ;x = [ebp + 8], y = [ebp + 12], z = [ebp + 16]
|  ;a = [ebp - 4] = [esp + 8], b = [ebp - 8] = [esp + 4], c = [ebp - 12] =   [esp]
|  mov ebp, esp
|  push ebp
 

Referanslar:


1
Cevabınız için teşekkür ederim. Ayrıca bağlantılar gerçekten harika ve bilgisayarların gerçekte nasıl çalıştığına dair hiç bitmeyen soruya daha fazla ışık tutmama yardımcı oluyor :)
Christoph

"Mevcut EBP değerini yığına iter" ile Ne Demek İstiyorsunuz ve ayrıca yığın işaretçisi kayıt defterinde saklanır veya yığın içinde bir konum işgal eder ... Biraz kafam karıştı
Suraj Jain

Ve bu * [ebp + 8], [ebp + 8] olmamalı mı?
Suraj Jain

@Suraj Jain; EBPVe ESPnedir biliyor musun ?
haccks

esp, yığın göstericisidir ve ebp temel göstericidir. Eksik bir bilgim varsa lütfen düzeltin.
Suraj Jain

19

Diğerlerinin de belirttiği gibi, kapsam dışına çıkana kadar parametrelerin popülasyonuna gerek yoktur.

Nick Parlante'nin "Pointers and Memory" den bazı örneklerini yapıştıracağım. Durumun düşündüğünüzden biraz daha basit olduğunu düşünüyorum.

İşte kod:

void X() 
{
  int a = 1;
  int b = 2;

  // T1
  Y(a);

  // T3
  Y(b);

  // T5
}

void Y(int p) 
{
  int q;
  q = p + 2;
  // T2 (first time through), T4 (second time through)
}

Zaman içindeki noktalar T1, T2, etc. kodda işaretlenmiştir ve o andaki hafıza durumu çizimde gösterilmiştir:

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


2
Harika görsel açıklama. Google'da araştırdım ve kağıdı burada buldum: cslibrary.stanford.edu/102/PointersAndMemory.pdf Gerçekten faydalı kağıt!
Christoph

7

Farklı işlemciler ve diller birkaç farklı yığın tasarımı kullanır. Hem 8x86 hem de 68000 üzerindeki iki geleneksel model Pascal çağırma kuralı ve C çağırma kuralı olarak adlandırılır; kayıtların adları dışında her bir kural her iki işlemcide de aynı şekilde ele alınır. Her biri, yığını ve ilişkili değişkenleri yönetmek için yığın işaretçisi (SP veya A7) ve çerçeve işaretçisi (BP veya A6) olarak adlandırılan iki kayıt kullanır.

Herhangi bir kuralı kullanarak alt yordamı çağırırken, yordam çağrılmadan önce yığına herhangi bir parametre itilir. Yordamın kodu daha sonra çerçeve işaretçisinin geçerli değerini yığına iter, yığın işaretçisinin geçerli değerini çerçeve işaretçisine kopyalar ve yığın işaretçisinden yerel değişkenler [varsa] tarafından kullanılan bayt sayısını çıkarır. Bu yapıldıktan sonra, ek veriler yığına gönderilse bile, tüm yerel değişkenler, yığın işaretçisinden sabit bir negatif yer değiştirmeye sahip değişkenlerde depolanır ve yığın üzerinde çağıran tarafından itilen tüm parametrelere bir çerçeve işaretçisinden sabit pozitif yer değiştirme.

İki kural arasındaki fark, alt yordamdan bir çıkışı işleme biçimlerinde yatmaktadır. C kuralında, geri döndürme işlevi, çerçeve işaretçisini yığın işaretçisine kopyalar [eski çerçeve işaretçisine basıldıktan hemen sonra sahip olduğu değere geri yükler], eski çerçeve işaretçisi değerini açar ve bir dönüş gerçekleştirir. Arayanın çağrıdan önce yığına bastırdığı parametreler orada kalacaktır. Pascal kuralında, eski çerçeve işaretçisini açtıktan sonra, işlemci işlev dönüş adresini açar, yığın işaretçisine çağıran tarafından itilen parametrelerin bayt sayısını ekler ve ardından açılan dönüş adresine gider. Orijinal 68000'de, arayanın parametrelerini kaldırmak için 3 komutlu bir dizi kullanmak gerekliydi; 8x86 ve orijinalden sonraki tüm 680x0 işlemciler bir "ret N" içeriyordu

Pascal kuralı, arayan tarafında bir miktar kod kaydetme avantajına sahiptir, çünkü arayanın bir işlev çağrısından sonra yığın işaretçisini güncellemesi gerekmez. Bununla birlikte, çağrılan işlevin, çağıranın yığına kaç baytlık parametre koyacağını tam olarak bilmesini gerektirir. Pascal kuralını kullanan bir işlevi çağırmadan önce yığına doğru sayıda parametrenin itilmesinin başarısız olması neredeyse bir çökmeye neden olur. Bununla birlikte, bu, çağrılan her yöntemin içindeki biraz fazladan kodun, yöntemin çağrıldığı yerlerde kodu kaydedeceği gerçeğiyle dengelenir. Bu nedenle, orijinal Macintosh araç kutusu rutinlerinin çoğu Pascal çağırma kuralını kullandı.

C çağırma kuralı, rutinlerin değişken sayıda parametreyi kabul etmesine izin verme ve bir rutin geçirilen tüm parametreleri kullanmasa bile sağlam olma avantajına sahiptir (arayan, kaç bayt değerinde parametre ittiğini bilecektir ve böylece onları temizleyebilecek). Ayrıca, her işlev çağrısından sonra yığın temizliği yapmak gerekli değildir. Bir rutin, her biri dört bayt değerinde parametre kullanan sırayla dört işlevi ADD SP,4çağırırsa, her aramadan sonra kullanmak yerine ADD SP,16, parametreleri dört aramadan temizlemek için son aramadan sonra birini kullanabilir .

Günümüzde tarif edilen çağrı gelenekleri biraz modası geçmiş kabul edilmektedir. Derleyiciler yazmaç kullanımında daha verimli hale geldiklerinden, tüm parametrelerin yığın üzerinde itilmesini gerektirmek yerine yazmaçlarda birkaç parametre kabul eden yöntemlere sahip olmak yaygındır; bir yöntem tüm parametreleri ve yerel değişkenleri tutmak için yazmaçları kullanabilirse, bir çerçeve işaretçisi kullanmaya gerek yoktur ve bu nedenle eskisini kaydetmeye ve geri yüklemeye gerek yoktur. Yine de, onları kullanmak için bağlantılı kitaplıkları ararken bazen eski arama kurallarını kullanmak gerekir.


1
Vaov! Beynini bir hafta kadar ödünç alabilir miyim? Bazı nitty-cesur şeyler çıkarmanız gerekiyor! Mükemmel cevap!
Christoph

Çerçeve ve yığın işaretçisi yığının kendisinde veya başka bir yerde nerede saklanır?
Suraj Jain

@SurajJain: Tipik olarak, çerçeve işaretçisinin kaydedilen her kopyası, yeni çerçeve işaretçi değerine göre sabit bir yer değiştirmede saklanacaktır.
supercat

Efendim, bu şüpheyi uzun zamandır yaşıyorum. Eğer fonksiyonumda eğer (g==4)o zaman yazarsam int d = 3ve sonra gkullanarak girdi scanfalırsam başka bir değişken tanımlarım int h = 5. Şimdi, derleyici şimdi d = 3yığına nasıl yer veriyor ? Offset nasıl yapılır, çünkü eğer gdeğilse 4, o zaman yığında d için bellek olmayacak ve basitçe ofset verilecek hve eğer g == 4o zaman ofset önce g için ve sonra için olacaktır h. Derleyici bunu derleme sırasında nasıl yapıyor, bizim g
girdimizi

@SurajJain: C'nin ilk sürümleri, bir işlev içindeki tüm otomatik değişkenlerin herhangi bir çalıştırılabilir ifadeden önce görünmesini gerektiriyordu. Bu karmaşık derlemeyi biraz gevşetmekle birlikte, bir yaklaşım, SP'den ileri bildirilmiş bir etiketin değerini çıkaran bir işlevin başlangıcında kod üretmektir. İşlev içinde, derleyici, kodun her noktasında, kaç bayt değerinde yerelin hala kapsamda olduğunu ve aynı zamanda kapsam dahilindeki yerellerin maksimum bayt sayısını izleyebilir. Fonksiyonunun sonunda, önceki ... değerini sağlayabilmektedir
SuperCat

5

Zaten burada gerçekten çok iyi cevaplar var. Bununla birlikte, yığının LIFO davranışıyla ilgili hala endişeleriniz varsa, bunu bir değişkenler yığını yerine bir çerçeve yığını olarak düşünün. Önermek istediğim, bir işlev yığının en üstünde olmayan değişkenlere erişebilse de, yine de yalnızca yığının en üstündeki öğe üzerinde çalışıyor : tek bir yığın çerçevesi.

Elbette bunun istisnaları var. Tüm çağrı zincirinin yerel değişkenleri hala tahsis edilmiştir ve kullanılabilir durumdadır. Ancak bunlara doğrudan erişilmeyecek. Bunun yerine, referansla (veya anlamsal olarak gerçekten farklı olan işaretçi ile) aktarılırlar. Bu durumda, çok daha aşağıda bir yığın çerçevesinin yerel bir değişkenine erişilebilir. Ancak bu durumda bile, halihazırda yürütülen işlev hala yalnızca kendi yerel verileri üzerinde çalışmaktadır. Kendi yığın çerçevesinde depolanan bir referansa erişiyor; bu, yığın üzerindeki, statik bellekteki veya yığının daha aşağısındaki bir şeye referans olabilir.

Bu, yığın soyutlamasının, işlevleri herhangi bir sırada çağrılabilir kılan ve özyinelemeye izin veren bölümüdür. Üst yığın çerçevesi, kod tarafından doğrudan erişilen tek nesnedir. Diğer her şeye dolaylı olarak erişilir (üst yığın çerçevesinde bulunan bir işaretçi aracılığıyla).

Küçük programınızın montajına bakmak öğretici olabilir, özellikle optimizasyon yapmadan derlerseniz. Sanırım işlevinizdeki tüm bellek erişiminin yığın çerçeve işaretçisinden bir uzaklık aracılığıyla gerçekleştiğini göreceksiniz, bu işlevin kodunun derleyici tarafından nasıl yazılacağıdır. Referansla geçiş durumunda, yığın çerçeve işaretçisinden bir ofsette saklanan bir işaretçi aracılığıyla dolaylı bellek erişim talimatlarını görürsünüz.


4

Çağrı yığını aslında bir yığın veri yapısı değildir. Kullandığımız bilgisayarlar perde arkasında rastgele erişimli makine mimarisinin uygulamalarıdır. Böylece, a ve b'ye doğrudan erişilebilir.

Makine perde arkasında şunları yapar:

  • get "a", yığının üst kısmının altındaki dördüncü öğenin değerini okumaya eşittir.
  • get "b", yığının üst kısmının altındaki üçüncü öğenin değerini okumaya eşittir.

http://en.wikipedia.org/wiki/Random-access_machine


1

İşte C'nin çağrı yığını için oluşturduğum bir diyagram. Google görsel sürümlerinden daha doğru ve çağdaş

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

Ve yukarıdaki diyagramın tam yapısına karşılık, burada Windows 7'de notepad.exe x64'ün bir hata ayıklaması var.

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

Düşük adresler ve yüksek adresler değiştirilir, böylece yığın bu diyagramda yukarı doğru tırmanır. Kırmızı, kareyi tam olarak ilk diyagramdaki gibi gösterir (kırmızı ve siyah kullanılmış, ancak şimdi siyah yeniden kullanılmıştır); siyah ev alanıdır; mavi, aramadan sonra talimata arayan işlevi için bir sapma olan dönüş adresidir; turuncu, hizalamadır ve pembe, talimat işaretçisinin aramadan hemen sonra ve ilk talimattan önce işaret ettiği yerdir. Ana alan + dönüş değeri, pencerelerde izin verilen en küçük çerçevedir ve çağrılan işlevin hemen başlangıcındaki 16 bayt rsp hizalaması sürdürülmesi gerektiğinden, bu her zaman 8 baytlık bir hizalamayı da içerir.BaseThreadInitThunk ve bunun gibi.

Kırmızı işlev çerçeveleri, aranan işlevin mantıksal olarak neye sahip olduğunu + okur / değiştirdiğini gösterir (yığın üzerinde iletilen bir parametreyi -Ofast üzerinde bir kayıtta geçirmek için çok büyük olan değiştirebilir). Yeşil çizgiler, işlevin kendisine ayırdığı alanı işlevin başından sonuna kadar sınırlar.


RDI ve diğer yazmaç argümanları, yalnızca hata ayıklama modunda derlerseniz yığına tamamen dökülür ve bir derlemenin bu sırayı seçeceğinin garantisi yoktur. Ayrıca, en eski işlev çağrısı için neden yığın argümanları diyagramın üstünde gösterilmiyor? Diyagramınızda hangi çerçevenin hangi veriye "sahip olduğu" arasında net bir sınır yok. (Aranan uç, yığın değişkenlerine sahiptir). Yığın değiştirgelerini diyagramın üstünden çıkarmak, "yazmaçlarda geçirilemeyen parametrelerin" her zaman her işlevin dönüş adresinin hemen üstünde olduğunu görmeyi daha da zorlaştırır.
Peter Cordes

@PeterCordes goldbolt asm çıktısı, clang ve gcc callees'i varsayılan davranış olarak yığına bir kayıtta geçirilen bir parametreye iten gösterir, bu yüzden bir adresi vardır. Gcc'de registerparametrenin arkasında kullanmak bunu optimize eder, ancak adresin işlev içinde asla alınmadığı için zaten optimize edileceğini düşünürsünüz. Üst çerçeveyi düzelteceğim; kuşkusuz elipsleri ayrı bir boş çerçeveye koymalıydım. 'bir aranan uç kendi yığın değiştirgelerine sahiptir', ya arayanın kayıtlara geçirilemezlerse ittikleri dahil?
Lewis Kelsey

Evet, optimizasyon devre dışı bırakılmış olarak derlerseniz, aranan uç onu bir yere dökecektir. Ancak yığın bağımsız değişkenlerinin (ve muhtemelen kaydedilmiş RBP'nin) aksine, hiçbir şey nerede olduğu konusunda standartlaştırılmamıştır. Re: callee kendi yığın değiştirgelerine sahiptir: evet, işlevlerin gelen değiştirgelerini değiştirmelerine izin verilir. Kendini döktüğü reg argümanlar yığın argümanları değildir. Derleyiciler bazen bunu yapar, ancak IIRC genellikle arg'yi yeniden okumasalar bile dönüş adresinin altındaki alanı kullanarak yığın alanını boşa harcarlar. Arayan bir kişi aynı değiştirgelerle başka bir arama yapmak isterse, güvende olmak için tekrar etmeden önce başka bir kopya kaydetmelidircall
Peter Cordes

@PeterCordes Pekala, argümanları arayan yığının bir parçası yaptım çünkü yığın çerçevelerini rbp noktalarına göre sınırlandırıyordum. Bazı diyagramlar bunu aranan uç yığınının bir parçası olarak gösterir (bu sorudaki ilk diyagramda olduğu gibi) ve bazıları bunu arayan yığının bir parçası olarak gösterir, ancak bunları parametre kapsamı olarak görerek aranan uç yığınının bir parçası yapmak mantıklı olabilir. Arayan için üst düzey kodda erişilebilir değil. Evet, öyle görünüyor registerve constoptimizasyonlar sadece -O0'da bir fark yaratıyor.
Lewis Kelsey

@PeterCordes Ben değiştirdim. Yine de değiştirebilirim
Lewis Kelsey
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.