Путаница с ростом стека в Linux x86_64 - PullRequest
0 голосов
/ 28 октября 2018

Я пытаюсь полностью понять механизм наращивания стека при вызове функций, и я немного запутался Для лучшего понимания я написал следующую простую программу:

#include <stdio.h>
#include <stdint.h>

void callee(uint32_t* p)
{
    uint32_t tmp = 9;
    printf("callee - tmp is located at address location:%p and p is:%p \n", &tmp, p);
}

void caller()
{
    uint32_t tmp1 = 12;
    printf("caller - address of tmp1:%p \n", &tmp1);
    calle(&tmp1);
}

int main(int argc, char** argv)
{
    caller();
    return 0;
}

И, используя онлайн-конвертер , я получил следующий вывод сборки (я оставил только код функции callee):

.LC0:
    .string "callee - tmp is located at address location:%p and p is:%p \n"
calle:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 32 // command 1
    mov     QWORD PTR [rbp-24], rdi
    mov     DWORD PTR [rbp-4], 9 // command 2
    mov     rdx, QWORD PTR [rbp-24]
    lea     rax, [rbp-4]
    mov     rsi, rax
    mov     edi, OFFSET FLAT:.LC0
    mov     eax, 0
    call    printf
    nop
    leave
    ret

Как я понимаю, принимая во внимание команды 1 и 2 (отмеченные выше), стек действительно уменьшается к младшим адресам и (примерному) выводу скомпилированного кода, когда я компилирую его с использованием команда gcc myProg.c -o prog выглядит следующим образом:

абонент - адрес tmp1: 0x7ffe423e8ed4

callee - tmp находится по адресу: 0x7ffe423e8eb4 и p: 0x7ffe423e8ed4

Где видно, что локальная переменная, выделенная в функции callee, расположена в более низком адресе памяти, чем локальная переменная в функции caller. Так что хорошо.

И все же , когда я компилирую программу с опцией -O2 (т.е.: gcc -O2 myProg.c -o prog), (пример) вывод скомпилированного кода выглядит примерно так:

абонент - адрес tmp1: 0x7fff0d5bfa90

callee - tmp находится по адресу: 0x7fff0d5bfa94 и p: 0x7fff0d5bfa90

Что на этот раз показывает, что локальная переменная, выделенная в кадре стека callee, расположена в более высоком адресе памяти, чем локальная переменная в функции caller.

Так что мой вопрос - опция оптимизации -O2 оптимизирует "до" ситуации, когда механизм роста стека действительно изменяется или я что-то здесь упускаю ...?

gcc версия: 7.3

архитектура: x86_64

ОС: Ubuntu 18.04.

Ценю ваши разъяснения.

Guy.

Ответы [ 2 ]

0 голосов
/ 29 октября 2018

Поскольку вызов printf из calle оптимизирован для функции caller, см. godbolt .

Выход сборки для gcc 7.3 -O2:

.LC0:
        .string "calle - tmp is located at address location:%p and p is:%p \n"
calle:
        sub     rsp, 24
        mov     rdx, rdi
        xor     eax, eax
        lea     rsi, [rsp+12]
        mov     edi, OFFSET FLAT:.LC0
        mov     DWORD PTR [rsp+12], 9
        call    printf
        add     rsp, 24
        ret
.LC1:
        .string "caller - address of tmp1:%p \n"
caller:
        sub     rsp, 24
        mov     edi, OFFSET FLAT:.LC1
        xor     eax, eax
        lea     rsi, [rsp+8]
        mov     DWORD PTR [rsp+8], 12
        call    printf
        lea     rdx, [rsp+8]
        lea     rsi, [rsp+12]
        mov     edi, OFFSET FLAT:.LC0
        xor     eax, eax
        mov     DWORD PTR [rsp+12], 9
        call    printf
        add     rsp, 24
        ret
main:
        sub     rsp, 8
        xor     eax, eax
        call    caller
        xor     eax, eax
        add     rsp, 8
        ret

Как видите, функция calle была встроена в caller, поэтому функция caller вызывает printf два раза, сначала со строкой LC1, затем со строкой LC0.Первый раз он печатает адрес rsp+8, который равен tmp1, второй раз с rsp+12, который равен tmp2.GCC может свободно выбирать порядок переменных, которые он выбирает.

Вы можете установить атрибут __attribute__((__noinline__)) в calle, чтобы "исправить" это, но ... вы не должны ожидать, что адреса переменных будут иметь какие-либопорядок вообще (кроме случаев, когда это возможно, например, для массивов и структур).

PS Вызов модификатора "%p" printf без указателя void* технически не определен, поэтому вы должны привести printf arg к void*перед печатью.printf("caller - address of tmp1:%p \n", (void*)&tmp1);

0 голосов
/ 28 октября 2018

-O2 встроенные функции, после чего компилятор может свободно делать выделение стека, как ему захочется.

Сравнение адресов между отдельными объектами (например, tmp и tmp1) является технически неопределенным поведением в C, поэтому любой тип > или < отношений между адресами, основанный на вложении функций, равен не наблюдаемый побочный эффект, который должна сохранить оптимизация при соблюдении правила «как будто» . Компиляторы даже не пытаются сделать это при встраивании функций.

ИСО С11, черновик n1548 , §6.5.8 Реляционные операторы

5) При сравнении двух указателей результат зависит от относительного расположения в адресное пространство указанных объектов. Если два указателя на типы объектов оба указывают на один и тот же объект, или оба указывают один за последним элементом того же объекта массива, они сравнить равных. Если указанные объекты являются членами одного и того же агрегатного объекта , указатели на элементы структуры, объявленные позже, сравнивают больше, чем указатели на элементы объявлено ранее в структуре, и указатели на элементы массива с большим индексом значения сравнивают больше, чем указатели, с элементами того же массива с нижним индексом ценности. Все указатели на члены одного и того же объекта объединения сравниваются одинаково. Если Выражение P указывает на элемент объекта массива, а выражение Q указывает на последний элемент того же объекта массива, выражение указателя Q + 1 сравнивается больше, чем P. Во всех остальных случаях поведение не определено

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

...