2017-09-29 66 views
6

Co jest za kulisami powrotu wartości z funkcji w C++?Za kulisami zwracanej wartości z funkcji C++

W moim rozumieniu, gdy func. nazywany jest adresem powracającym i ramką stosu (ze zmiennymi lokalnymi, wielokrotnie powtarzaną kolejnością argumentów i rejestrów) jest wypychany na stos wywoławczy.

Ale co się dzieje, gdy wykonywanie spotkań zwraca statemenet? np

int a(int b){ 
    int c = b * 2; 
    return c; 
} 

Po napotkaniu return jest wartość C przechowywane w EAX rejestru zmienne lokalne są zniszczone, a rama stos zostaje usunięty z połączenia stosie, po czym wartość EAX rejestru przemieszcza się „powrotu adres” pamięć?

Czy źle zrozumiałem tę koncepcję?

Cała pomoc jest wysoko ceniona. Dzięki.

+0

Funkcja wywołująca po prostu użyje EAX (lub gdziekolwiek) – Caleth

+5

Czy chcesz wiedzieć, co dzieje się w niezoptymalizowanym przypadku? Pytam, ponieważ wystarczająco dobry kompilator mógłby włączyć 'int foo = a (2);' do bezpośredniego inicjowania 'foo' z' 4'. – NathanOliver

+2

Poinformuj kompilator, aby wyprowadził język asemblera dla funkcji. –

Odpowiedz

4

BTW, język asemblera zależy od procesora. Procesor ARM nie ma rejestru EAX.

Kompilatory mogą mieć standard przekazywania parametrów i zwracania parametrów. Metoda zwracania wartości z funkcji zależy od implementacji (kompilatora). Nie ma standardu w kompilatorach wszystkich.

kod Niezoptymalizowana
kompilatory są przeznaczone do wykorzystania rejestrów procesora.

Jeśli wartość zwracana pasuje do pojedynczego rejestru, to zostanie użyty rejestr w celu zwrócenia wartości. Zależy od procesora.

Dla większych obiektów/wartości, kompilator ma dwie opcje: zwrócić obiekt w wielu rejestrach lub zwrócić wskaźnik do wartości. Wskaźnik może być tak prosty, jak indeks do stosu lub adres do miejsca, w którym znajduje się wartość.

zoptymalizowany kod
Kompilator może zastąpić swoją funkcję z prostą instrukcją procesora lub nawet spaść kod. W tym przypadku nie ma wartości zwracanej.

Kompilator może ocenić swoją funkcję na stałą i umieścić stałą w kodzie wykonywalnym; w związku z tym nie wymaga żadnych wywołań funkcji lub funkcji.

Kompilator może zadecydować o wbudowanej Twojej funkcji. W tym przypadku nie ma wartości zwracanej, podobnie jak instrukcja przypisania. Zmienna tymczasowa może być używana do przechowywania wartości lub innego rejestru.

Dalsze informacje
Bardziej szczegółowe informacje, badania "teorii kompilacji". Jest tam fajna książka ze smokiem ...

+0

Prawdopodobnie jedyny czas w twoim życiu programistycznym, w którym "Here be Dragons" jest dobrą rzeczą. – user4581301

2

C++ jest określona w kategoriach operacji w stosunku do teoretycznego modelu pamięci komputera.

Posiada również funkcję znaną jako "jak gdyby".Oznacza to, że kompilator może generować dowolny kod, pod warunkiem, że ogólny obserwowalny efekt jest "taki, jak gdyby" kod, który napisałeś, został dosłownie przetłumaczony na operacje w stosunku do modelu pamięci.

W kodzie nieoptymalizowane, monter produkowane jest w rzeczywistości bardzo blisko do operacji wyrażonej w kodzie, na przykład gcc może produkować następujący kod dla funkcji:

a(int):         # @a(int) 
     push rbp      
     mov  rbp, rsp     
     mov  dword ptr [rbp - 4], edi 
     mov  edi, dword ptr [rbp - 4] 
     shl  edi, 1 
     mov  dword ptr [rbp - 8], edi 
     mov  eax, dword ptr [rbp - 8] 
     pop  rbp 
     ret 

i biorąc pod uwagę następujący kod wywołujący:

extern void foo(int x); 

int main() 
{ 
    foo(a(2)); 
} 

następujący kod może być wytwarzane:

main:         # @main 
     push rbp 
     mov  rbp, rsp 
     mov  edi, 2 
     call a(int) 
     mov  edi, eax 
     call foo(int) 
     xor  eax, eax 
     pop  rbp 
     ret 

W tym prostym programie, zauważalnym efektem kodu jest wywołanie foo z argumentem o wartości 4. Połączenie z a ma tylko jeden obserwowalny efekt uboczny. Oznacza to, że jego wartość zwracana jest dwukrotnie większa od wartości wejściowej.

Ponieważ zwracana wartość jest przekazywana bezpośrednio do foo i nie są przechowywane w dowolnym miejscu, moglibyśmy powiedzieć, że wszystkie skutki uboczne nazywając a są całkowicie zużywane przez wywołanie foo.

Dlatego jeśli kompilator wie, co robi a, nie musi generować kodu, aby go wywołać. Może po prostu zadzwonić pod numer foo z wartością wyprowadzoną "tak jakby", wywołując a(2).

Rzeczywiście, dodając optymalizacji daje nam to:

main:         # @main 
     push rax 
     mov  edi, 4   # note: 'as if' a(2) 
     call foo(int) 
     xor  eax, eax 
     pop  rcx 
     ret 

Realizacja a w tym przypadku (na gcc) jest następujący:

a(int):         # @a(int) 
# 'as if' we created a variable and did some arithmetic, 
# stored the result and then returned the result 
     lea  eax, [rdi + rdi] 
     ret 
1

Jeśli proszą w perspektywie ciekawości @Thomas Odpowiedź Matthewsa jest bardzo dobra ...

Jeśli masz pytanie dotyczące konkretnego scenariusza, musisz powinien sprawdź sam i zobacz wyniki, odczytanie kodu montażowego jest trudne, ale na pewno satysfakcjonujące.

Na przykład, opracowano następujący przykład za pomocą gcc:

int func(int a, int b) 
{ 
    return a + b; 
} 

int main(int argc, char ** argv) 
{ 
    int a, b; 
    a = b = 100; 

    int c = func(a, b); 
} 

Jak widać, jest to tak proste, jak to się robi (Wskazówka: podczas demontażu, spróbuj uniknąć printf jak dodaje dużo montażu kod).

skompilować z -ggdb tak że łatwiej będzie pracować z gdb i uruchomić za pomocą gdb <application>, to wystarczy dodać punkt przerwania wewnątrz metody, czekać na to, aby uderzyć i uruchomić polecenie disassemble. Wyjście będzie wyglądać mniej więcej tak:

Breakpoint 1, func (a=100, b=100) at program.cpp:3 
3   return a + b; 
(gdb) disas 
Dump of assembler code for function func(int, int): 
    0x00000000004004d6 <+0>:  push %rbp 
    0x00000000004004d7 <+1>:  mov %rsp,%rbp 
    0x00000000004004da <+4>:  mov %edi,-0x4(%rbp) 
    0x00000000004004dd <+7>:  mov %esi,-0x8(%rbp) 
=> 0x00000000004004e0 <+10>: mov -0x4(%rbp),%edx 
    0x00000000004004e3 <+13>: mov -0x8(%rbp),%eax 
    0x00000000004004e6 <+16>: add %edx,%eax 
    0x00000000004004e8 <+18>: pop %rbp 
    0x00000000004004e9 <+19>: retq 
End of assembler dump. 

Jak widać, tutaj jedyną rzeczą kompilator robi to pop stary wskaźnik bazowy na RBP (pop %rbp), a następnie wrócić do naszego adresu zwrotnego (retq). Wynik jest już zapisany w rejestrze, więc nie ma potrzeby robienia czegokolwiek innego.