2016-11-28 8 views
1

Я посмотрел на выходе ассемблера для следующего фрагмента кода, и я был поражен:Lambdas как закрытие брать окружающая среда. Решающая роль 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.

+7

RIP относительная адресация была введена в x86_64. Ссылка на память относится к указателю инструкции. Это полезно при создании независимого от позиции кода. Вы можете найти описание здесь: https://www.tortall.net/projects/yasm/manual/html/nasm-effaddr.html –

+0

http://stackoverflow.com/q/18447627/995714, http: // stackoverflow .com/q/3250277/995714 –

+0

PIC хорош в 64-битной среде, так как операторы могут быть 32-разрядными. –

ответ

3

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

Таким образом, относительная адресация 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 почти всегда плохая идея.

+0

«Обратите внимание, что механизм захвата не продлевает срок хранения переменных захвата (например, в ECMAScript), поэтому захват локальной переменной var по ссылке в lambda для std :: thread почти всегда является плохой идеей». Да, но я захватил местных жителей, и мои потоки соединяются до «вне сферы действия». @Margaret спасибо! :) – Gilgamesz

+2

@Gilgamesz Вот почему я использовал почти всегда :) –

+0

Даже с NASM 'DEFAULT ABS' или эквивалентом для других ассемблеров' mov eax, [var] 'не собирается в [' A1 moffs64'] (http: //www.felixcloutier.com/x86/MOV.html). Абсолютная '[sign-extended-disp32]' адресация по-прежнему доступна в x86-64, и это то, что вы получаете от 'mov eax, [abs var]'. (x86-32 имел два избыточных способа кодирования. AMD64 использовал более короткий для RIP-относительный, оставив более длинный.) Я думаю, что в NASM вам придется писать 'mov eax, [qword var]', чтобы получить то, что GAS называет «movabs». –