Как работает возврат значений из функции? - PullRequest
6 голосов
/ 11 марта 2012

У меня недавно была серьезная ошибка, из-за которой я забыл вернуть значение в функцию. Проблема заключалась в том, что, несмотря на то, что ничего не было возвращено, он работал нормально под Linux / Windows и только при Mac. Я обнаружил ошибку, когда включил все предупреждения компилятора.

Итак, вот простой пример:

#include <iostream>

class A{
public:
    A(int p1, int p2, int p3): v1(p1), v2(p2), v3(p3)
    {
    }

    int v1;
    int v2;
    int v3;
};

A* getA(){
    A* p = new A(1,2,3);
//  return p;
}

int main(){

    A* a = getA();

    std::cerr << "A: v1=" << a->v1 << " v2=" << a->v2 << " v3=" << a->v3 << std::endl;  

    return 0;
}

Мой вопрос: как это может работать в Linux / Windows без сбоев? Как происходит возврат значений на более низкий уровень?

Ответы [ 8 ]

7 голосов
/ 11 марта 2012

В архитектуре Intel простые значения (целые и указатели) обычно возвращаются в регистр eax. Этот регистр (среди прочего) также используется как временное хранилище при перемещении значений в памяти и как операнд во время вычислений. Поэтому любое значение, оставленное в этом регистре, рассматривается как возвращаемое значение, и в вашем случае оно оказалось именно тем, что вы хотели вернуть.

3 голосов
/ 11 марта 2012

Вероятно, по счастливой случайности, в регистре осталось «а», которое используется для возврата результатов одного указателя, что-то в этом роде.

Вызовы / соглашения и возврат результатов функций зависят от архитектуры, поэтому неудивительно, что ваш код работает на Windows / Linux, но не на Mac.

2 голосов
/ 11 марта 2012

Для компилятора есть два основных способа вернуть значение:

  1. Поместить значение в регистр перед возвратом и
  2. Иметь вызывающегопередать блок памяти стека для возвращаемого значения и записать значение в этот блок [подробнее]

# 1 обычно используется со всем, что вписывается в регистр;# 2 для всего остального (большие структуры, массивы и так далее).

В вашем случае компилятор использует # 1 как для возврата new, так и для возврата вашей функции .В Linux и Windows компилятор не выполнял никаких операций искажения значений в регистре с возвращенным значением между записью его в переменную-указатель и возвратом из вашей функции;на Mac это сделал.Отсюда и разница в результатах, которые вы видите: в первом случае оставшееся значение в регистре возврата произошло вместе со значением, которое вы хотели вернуть в любом случае.

2 голосов
/ 11 марта 2012

Прежде всего, вам нужно немного изменить ваш пример, чтобы он скомпилировался. Функция должна иметь хотя бы путь выполнения, который возвращает значение.

A* getA(){
    if(false)
        return NULL;
    A* p = new A(1,2,3);
//  return p;
}

Во-вторых, это явно неопределенное поведение, что означает, что все может произойти, но я думаю, что этот ответ вас не удовлетворит.

В-третьих, в Windows он работает в режиме отладки, но если вы компилируете его в Release, он этого не делает.

В Debug скомпилировано следующее:

    A* p = new A(1,2,3);
00021535  push        0Ch  
00021537  call        operator new (211FEh) 
0002153C  add         esp,4 
0002153F  mov         dword ptr [ebp-0E0h],eax 
00021545  mov         dword ptr [ebp-4],0 
0002154C  cmp         dword ptr [ebp-0E0h],0 
00021553  je          getA+7Eh (2156Eh) 
00021555  push        3    
00021557  push        2    
00021559  push        1    
0002155B  mov         ecx,dword ptr [ebp-0E0h] 
00021561  call        A::A (21271h) 
00021566  mov         dword ptr [ebp-0F4h],eax 
0002156C  jmp         getA+88h (21578h) 
0002156E  mov         dword ptr [ebp-0F4h],0 
00021578  mov         eax,dword ptr [ebp-0F4h] 
0002157E  mov         dword ptr [ebp-0ECh],eax 
00021584  mov         dword ptr [ebp-4],0FFFFFFFFh 
0002158B  mov         ecx,dword ptr [ebp-0ECh] 
00021591  mov         dword ptr [ebp-14h],ecx 

Вторая инструкция, вызов operator new, перемещается в eax указатель на вновь созданный экземпляр.

    A* a = getA();
0010484E  call        getA (1012ADh) 
00104853  mov         dword ptr [a],eax 

Вызывающий контекст ожидает, что eax будет содержать возвращаемое значение, но это не так, он содержит последний указатель, присвоенный new, что, кстати, p.

Так вот почему это работает.

1 голос
/ 11 марта 2012

Как упомянул Kerrek SB, ваш код оказался в области неопределенного поведения.

По сути, ваш код собирается компилироваться до сборки.В сборке нет понятия функции, требующей возвращаемого типа, есть только ожидание.Я чувствую себя наиболее комфортно с MIPS, поэтому я буду использовать MIPS для иллюстрации.

Предположим, у вас есть следующий код:

int add(x, y)
{
    return x + y;
}

Это будет переводиться примерно так:

add:
    add $v0, $a0, $a1 #add $a0 and $a1 and store it in $v0
    jr $ra #jump back to where ever this code was jumped to from

Чтобы добавить 5 и 4, код будет называться примерно так:

addi $a0, $0, 5 # 5 is the first param
addi $a1, $0, 4 # 4 is the second param
jal add
# $v0 now contains 9

Обратите внимание, что в отличие от C, нет явного требования, что $ v0 содержит возвращаемое значение, простоожидание.Итак, что произойдет, если вы на самом деле ничего не вставляете в $ v0?Ну, $ v0 всегда имеет некоторое значение, поэтому значение будет таким, каким оно было в последний раз.

Примечание: В этом посте сделаны некоторые упрощения.Кроме того, ваш компьютер, скорее всего, не работает с MIPS ... Но, надеюсь, пример верен, и если вы изучили ассемблер в университете, MIPS может быть тем, что вы знаете в любом случае.

0 голосов
/ 11 марта 2012

При извлечении значений из стека в архитектуре IBM PC не происходит физического уничтожения старых значений данных, хранящихся там.Они просто становятся недоступными при работе стека, но остаются в той же ячейке памяти.

Конечно, предыдущие значения этих данных будут уничтожены при последующей загрузке новых данных в стек.

Так что, вероятно, вам просто повезло, и ничего не добавляется в стек во время вызова вашей функции и возврата окружающего кода.

0 голосов
/ 11 марта 2012

Что касается следующего утверждения из проекта стандарта C ++ n3242, параграф 6.6.3.2, ваш пример дает неопределенное поведение :

Выход из конца функции эквивалентен возврату без значение; это приводит к неопределенному поведению в возвращающем значение функция.

Лучший способ увидеть, что на самом деле происходит, - это проверить код сборки, сгенерированный данным компилятором в данной архитектуре. Для следующего кода:

#pragma warning(default:4716)
int foo(int a, int b)
{
    int c = a + b;
}

int main()
{
    int n = foo(1, 2);
}

... Компилятор VS2010 (в режиме отладки на 32-разрядной машине Intel) создает следующую сборку:

#pragma warning(default:4716)
int foo(int a, int b)
{
011C1490  push        ebp  
011C1491  mov         ebp,esp  
011C1493  sub         esp,0CCh  
011C1499  push        ebx  
011C149A  push        esi  
011C149B  push        edi  
011C149C  lea         edi,[ebp-0CCh]  
011C14A2  mov         ecx,33h  
011C14A7  mov         eax,0CCCCCCCCh  
011C14AC  rep stos    dword ptr es:[edi]  
    int c = a + b;
011C14AE  mov         eax,dword ptr [a]  
011C14B1  add         eax,dword ptr [b]  
011C14B4  mov         dword ptr [c],eax  
}
...
int main()
{
011C14D0  push        ebp  
011C14D1  mov         ebp,esp  
011C14D3  sub         esp,0CCh  
011C14D9  push        ebx  
011C14DA  push        esi  
011C14DB  push        edi  
011C14DC  lea         edi,[ebp-0CCh]  
011C14E2  mov         ecx,33h  
011C14E7  mov         eax,0CCCCCCCCh  
011C14EC  rep stos    dword ptr es:[edi]  
    int n = foo(1, 2);
011C14EE  push        2  
011C14F0  push        1  
011C14F2  call        foo (11C1122h)  
011C14F7  add         esp,8  
011C14FA  mov         dword ptr [n],eax  
}

Результат операции сложения в foo() сохраняется в регистре eax (аккумуляторе), а его содержимое используется в качестве возвращаемого значения функции, перемещаемой в переменную n.

eax используется для хранения возвращаемого значения (указателя) также в следующем примере:

#pragma warning(default:4716)
int* foo(int a)
{
    int* p = new int(a);
}

int main()
{
    int* pn = foo(1);

    if(pn)
    {
        int n = *pn;
        delete pn;
    }
}

Код сборки:

#pragma warning(default:4716)
int* foo(int a)
{
000C1520  push        ebp  
000C1521  mov         ebp,esp  
000C1523  sub         esp,0DCh  
000C1529  push        ebx  
000C152A  push        esi  
000C152B  push        edi  
000C152C  lea         edi,[ebp-0DCh]  
000C1532  mov         ecx,37h  
000C1537  mov         eax,0CCCCCCCCh  
000C153C  rep stos    dword ptr es:[edi]  
    int* p = new int(a);
000C153E  push        4  
000C1540  call        operator new (0C1253h)  
000C1545  add         esp,4  
000C1548  mov         dword ptr [ebp-0D4h],eax  
000C154E  cmp         dword ptr [ebp-0D4h],0  
000C1555  je          foo+50h (0C1570h)  
000C1557  mov         eax,dword ptr [ebp-0D4h]  
000C155D  mov         ecx,dword ptr [a]  
000C1560  mov         dword ptr [eax],ecx  
000C1562  mov         edx,dword ptr [ebp-0D4h]  
000C1568  mov         dword ptr [ebp-0DCh],edx  
000C156E  jmp         foo+5Ah (0C157Ah)  
std::operator<<<std::char_traits<char> >:
000C1570  mov         dword ptr [ebp-0DCh],0  
000C157A  mov         eax,dword ptr [ebp-0DCh]  
000C1580  mov         dword ptr [p],eax  
}
...
int main()
{
000C1610  push        ebp  
000C1611  mov         ebp,esp  
000C1613  sub         esp,0E4h  
000C1619  push        ebx  
000C161A  push        esi  
000C161B  push        edi  
000C161C  lea         edi,[ebp-0E4h]  
000C1622  mov         ecx,39h  
000C1627  mov         eax,0CCCCCCCCh  
000C162C  rep stos    dword ptr es:[edi]  
    int* pn = foo(1);
000C162E  push        1  
000C1630  call        foo (0C124Eh)  
000C1635  add         esp,4  
000C1638  mov         dword ptr [pn],eax  

    if(pn)
000C163B  cmp         dword ptr [pn],0  
000C163F  je          main+51h (0C1661h)  
    {
        int n = *pn;
000C1641  mov         eax,dword ptr [pn]  
000C1644  mov         ecx,dword ptr [eax]  
000C1646  mov         dword ptr [n],ecx  
        delete pn;
000C1649  mov         eax,dword ptr [pn]  
000C164C  mov         dword ptr [ebp-0E0h],eax  
000C1652  mov         ecx,dword ptr [ebp-0E0h]  
000C1658  push        ecx  
000C1659  call        operator delete (0C1249h)  
000C165E  add         esp,4  
    }
}

Компилятор VS2010 выдает предупреждение 4716 в обоих примерах. По умолчанию это предупреждение превращается в ошибку.

0 голосов
/ 11 марта 2012

Способ возврата значения из функции зависит от архитектуры и типа значения. Это может быть сделано через регистры или через стек. Обычно в архитектуре x86 значение возвращается в регистр EAX, если это целочисленный тип: char, int или pointer. Когда вы не указываете возвращаемое значение, это значение не определено. Это только ваша удача, что ваш код иногда работал правильно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...