Лямбды как затворы, принимающие окружающую среду. Решающая роль регистра RIP

Я посмотрел на вывод ассемблера для следующего фрагмента кода, и я был ошеломлен:

int x=0, y=0; // global
// r1, r2 are ints, local.
std::thread t([&x, &y, &r1, &r2](){
x = 1;
r1 = y;
});

!std::thread t([&x, &y, &r1, &r2](){
<lambda()>::operator()(void) const+0: push   %rbp
<lambda()>::operator()(void) const+1: mov    %rsp,%rbp
<lambda()>::operator()(void) const+4: mov    %rdi,-0x8(%rbp)
<lambda()>::operator()(void) const+18: mov    -0x8(%rbp),%rax
<lambda()>::operator()(void) const+22: mov    (%rax),%rax
!   x = 1;
<lambda()>::operator()(void) const()
<lambda()>::operator()(void) const+8: movl   $0x1,0x205362(%rip)        # 0x6062ac <x>
!   r1 = y;
<lambda()>::operator()(void) const+25: mov    0x205359(%rip),%edx        # 0x6062b0 <y>
<lambda()>::operator()(void) const+31: mov    %edx,(%rax)
!
!});
<lambda()>::operator()(void) const+33: nop
<lambda()>::operator()(void) const+34: pop    %rbp
<lambda()>::operator()(void) const+35: retq

Почему адрес x,y определяется относиться к RIP, RIP это указатель на инструкцию, поэтому он кажется диким. Тем более я никогда не видел ничего подобного. (Возможно, я не видел много вещей :)).

Единственное объяснение, которое приходит мне в голову, заключается в том, что лямбда-замыкание и взятие переменных среды из определенного места имеет что-то общее с RIP,

1

Решение

Код не перемещается во время выполнения, после загрузки секции кода подпрограмма не копируется и не перемещается.
Статические данные также занимают тот же адрес после загрузки их раздела.
Таким образом, расстояние между инструкцией и статической переменной известно во время компиляции, и оно является инвариантным при перемещении базы модуля (поскольку и инструкция, и данные переводятся на одну и ту же величину).

Таким образом, RIP-относительная адресация не только не является дикой, но она всегда отсутствовала.
В то время как в 32-битном коде инструкция вроде mov eax, [var] безвреден, в 64-битной без RIP-относительной адресации требуется 9 байтов, 1 для кода операции и 8 для немедленной.
С RIP-относительной адресацией непосредственные значения все еще 32-битные.


C ++ lamdbas — это синтаксический сахар для функционального объекта, где захваченные переменные становятся переменными экземпляра.
Переменные, захваченные по ссылке, обрабатываются как указатель / ссылка.
Глобальные переменные не требуют особой обработки при захвате, так как они уже доступны.

Вы справедливо отметили, что x а также y Доступ соответственно как 0x205362(%rip) а также 0x205359(%rip),
Поскольку они являются глобальными, их адрес фиксируется во время выполнения, и для доступа к ним используется относительная RIP-адресация.

Однако вы забыли проверить, как r1, локальная захваченная переменная, доступ.
Хранится с (%rax) а также rax был ранее загружен как (оптимизация) movq (%rdi), %rax,
%rdi это первый параметр метода operator(), так что, это thisтолько что упомянутая инструкция загружает первую переменную экземпляра в rax а затем использовать это значение для доступа r1,
Проще говоря, это указатель (или лучше ссылка) на r1, поскольку r1 живет в стеке, его адрес является динамическим во время выполнения (это зависит от состояния стека).

Таким образом, лямбда использует как косвенную, так и относительную к RIP адресацию, что противоречит гипотезе о том, что относительная к RIP адресация была какой-то особенной.


Обратите внимание, что механизм захвата не продлевает срок службы переменных захвата (как в ECMAScript), поэтому захват локальной переменной по ссылке в лямбда-выражении для std::thread почти всегда плохая идея.

3

Другие решения

Других решений пока нет …

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector