Используют ли языки/компиляторы инструкцию x86 ENTER с ненулевым уровнем вложенности?

Те, кто знаком с программированием сборки x86, очень привыкли к типичной функции пролог/эпилог:

push ebp
mov  esp, ebp
sub  esp, [size of local variables]
...
mov  esp, ebp
pop  ebp
ret

Эта же последовательность кода также может быть реализована с помощью инструкций ENTER и LEAVE:

enter [size of local variables], 0
...
leave
ret

Второй операнд ENTER инструкции - это уровень вложенности, который позволяет получить доступ к нескольким родительским кадрам из вызываемой функции.

Это не используется в C, потому что нет вложенных функций; локальные переменные имеют только область действия функции, в которой они объявлены. Эта конструкция не существует (хотя иногда мне это хотелось):

void func_a(void)
{
    int a1 = 7;

    void func_b(void)
    {
        printf("a1 = %d\n", a1);  /* a1 inherited from func_a() */
    }

    func_b();
}

Однако у Python есть вложенные функции, которые ведут себя следующим образом:

def func_a():
    a1 = 7
    def func_b():
        print 'a1 = %d' % a1      # a1 inherited from func_a()
    func_b()

Конечно, код Python не переводится непосредственно на машинный код x86 и, следовательно, не сможет (вряд ли?) воспользоваться этой инструкцией.

Существуют ли какие-либо языки, которые компилируются на x86 и предоставляют вложенные функции? Существуют ли компиляторы, которые будут генерировать инструкцию ENTER с ненулевым вторым операндом?

Intel инвестировала ненулевое количество времени/денег в этот операнд уровня вложенности, и в основном мне просто интересно, если кто-нибудь его использует: -)

Литература:

Ответ 1

enter на практике избегается, так как он работает довольно плохо - см. ответы "enter" vs "push ebp; mov ebp, esp; sub esp, imm" и "отпуск" vs "mov esp, ebp; pop ebp" . Есть куча инструкций x86, которые устарели, но по-прежнему поддерживаются по соображениям обратной совместимости. enter - один из них. (leave ОК, хотя компиляторы с удовольствием испускают его.)

Реализация вложенных функций в полной общности, как и в Python, на самом деле представляет собой значительно более интересную проблему, чем просто выбор нескольких команд управления кадрами - поиск "преобразования закрытия" и "проблемы с восходящей/нисходящей потерей", и вы найдете много интересных обсуждений.

Обратите внимание, что x86 изначально был спроектирован как машина Pascal, поэтому есть инструкции для поддержки вложенных функций (enter, leave), соглашения о вызове pascal, в котором вызываемая группа выдает известное количество аргументы из стека (ret K), проверка границ (bound) и т.д. Многие из этих операций теперь устарели.

Ответ 2

Как Iwillnotexist Idonotexist отметил, GCC поддерживает вложенные функции в C, используя точный синтаксис, который я показал выше.

Однако он не использует инструкцию ENTER. Вместо этого переменные, которые используются во вложенных функциях, группируются вместе в области локальных переменных, а указатель на эту группу передается вложенную функцию. Интересно, что этот "указатель на родительские переменные" передается через нестандартный механизм: на x64 он передается в r10, а на x86 (cdecl) он передается в ecx, который зарезервирован для указателя this в С++ (который в любом случае не поддерживает вложенные функции).

#include <stdio.h>
void func_a(void)
{
    int a1 = 0x1001;
    int a2=2, a3=3, a4=4;
    int a5 = 0x1005;

    void func_b(int p1, int p2)
    {
        /* Use variables from func_a() */
        printf("a1=%d a5=%d\n", a1, a5);
    }
    func_b(1, 2);
}

int main(void)
{
    func_a();
    return 0;
}

Производит следующий (фрагмент кода) при компиляции для 64-разрядного:

00000000004004dc <func_b.2172>:
  4004dc:   push   rbp
  4004dd:   mov    rbp,rsp
  4004e0:   sub    rsp,0x10
  4004e4:   mov    DWORD PTR [rbp-0x4],edi
  4004e7:   mov    DWORD PTR [rbp-0x8],esi
  4004ea:   mov    rax,r10                    ; ptr to calling function "shared" vars
  4004ed:   mov    ecx,DWORD PTR [rax+0x4]
  4004f0:   mov    eax,DWORD PTR [rax]
  4004f2:   mov    edx,eax
  4004f4:   mov    esi,ecx
  4004f6:   mov    edi,0x400610
  4004fb:   mov    eax,0x0
  400500:   call   4003b0 <[email protected]>
  400505:   leave  
  400506:   ret    

0000000000400507 <func_a>:
  400507:   push   rbp
  400508:   mov    rbp,rsp
  40050b:   sub    rsp,0x20
  40050f:   mov    DWORD PTR [rbp-0x1c],0x1001
  400516:   mov    DWORD PTR [rbp-0x4],0x2
  40051d:   mov    DWORD PTR [rbp-0x8],0x3
  400524:   mov    DWORD PTR [rbp-0xc],0x4
  40052b:   mov    DWORD PTR [rbp-0x20],0x1005
  400532:   lea    rax,[rbp-0x20]              ; Pass a, b to the nested function
  400536:   mov    r10,rax                     ; in r10 !
  400539:   mov    esi,0x2
  40053e:   mov    edi,0x1
  400543:   call   4004dc <func_b.2172>
  400548:   leave  
  400549:   ret  

Выход из objdump --no-show-raw-insn -d -Mintel

Это будет эквивалентно чему-то более подробному:

struct func_a_ctx
{
    int a1, a5;
};

void func_b(struct func_a_ctx *ctx, int p1, int p2)
{
    /* Use variables from func_a() */
    printf("a1=%d a5=%d\n", ctx->a1, ctx->a5);
}

void func_a(void)
{
    int a2=2, a3=3, a4=4;
    struct func_a_ctx ctx = {
        .a1 = 0x1001,
        .a5 = 0x1005,
    };

    func_b(&ctx, 1, 2);
}

Ответ 3

Наш PARLANSE компилятор (для мелкозернистых параллельных программ на SMP x86) имеет лексическое определение.

PARLANSE пытается сгенерировать много, много небольших параллельных зерен вычисления, а затем мультиплексирует их поверх потоков (1 на процессор). Фактически, фреймы стека выделены в кучу; мы не хотели платить цену за "большой стек" за каждое зерно, так как у нас их много, и мы не хотели ограничивать то, как глубоко можно что-либо повторить. Из-за параллельных вилок стек фактически представляет собой стек кактуса.

Каждая процедура, при входе, создает лексический дисплей, чтобы разрешить доступ к окружающим лексическим областям. Мы рассмотрели использование инструкции ENTER, но решили против нее по двум причинам:

  • Как отмечали другие, это не особенно быстро. Инструкции MOV так же хорошо.
  • Мы заметили, что дисплей часто разрежен и имеет тенденцию быть более плотным на лексически более глубокой стороне. Большинство внутренних вспомогательных функций отлично справляются с доступом только к их непосредственному лексическому родительскому элементу; вам не всегда нужен доступ ко всем вашим родителям. Иногда нет.

Следовательно, компилятор точно определяет, какие лексические области, к которым функция нуждается в доступе, и генерирует в пролог функции, куда должен идти ЕГЭ, только инструкции MOV, чтобы скопировать часть родительского отображения, которое действительно необходимо. Это часто оказывается 1 или 2 парами ходов.

Итак, мы выигрываем дважды по производительности, используя ENTER.

IMHO, ENTER теперь является одной из тех устаревших инструкций CISC, которая казалась хорошей идеей в то время, когда она была определена, но превзойдены последовательностями команд RISC, которые оптимизирует даже Intel x86.

Ответ 4

Я сделал некоторую статистику подсчета команд для загрузки Linux с использованием виртуальной платформы Simics и обнаружил, что ВВОД никогда не использовался. Однако в миксе было довольно много инструкций LEAVE. Между CALL и LEAVE была почти 1-1 корреляция. Казалось бы, это подтверждает идею о том, что ENTER просто медленный и дорогой, а LEAVE довольно удобен. Это было измерено на ядре 2.6 серии.

Те же эксперименты в ядре серии 4.4 и ряда 3.14 показали нулевое использование либо LEAVE, либо ENTER. Предположительно, генерация кода gcc для новых gcc, используемых для компиляции этих ядер, прекратила выдавать LEAVE (или параметры машины заданы по-разному).