Почему следующая разыменование не приводит к ошибке доступа к памяти?
Поскольку вы используете его как операнд памяти для блока asm, который не разыскивает его напрямую, только относительно базы сегмента GS. База GS установлена на любой виртуальный адрес, в котором мы хотим, чтобы блок локального хранилища потока этого потока был.
См. Как работает gcc `__thread`? и / или Адреса переменных локального хранилища потоков о том, как gcc в Linux реализует локальное хранилище потоков (TLS), используя регистр сегментов FS или GS.XNU явно делает то же самое, но использует встроенный asm вместо того, чтобы использовать встроенные функции GNU C. для потоковых данных.
Ограничение "m"
несколько похоже на оператор C &
: вместо этогозагружая объект в регистр, компилятор просто заменяет режим адресации, который ссылается на объект, в шаблон asm.
Поскольку этот шаблон asm не использует режим адресации напрямую, а вместо этого %%gs:
,на самом деле не делает разыменование *(void **)(slot * sizeof(void *)))
, которое могло бы произойти, если бы вы присвоили его переменной в чистом C.
подстановки asm-шаблона являются чисто текстовыми.Вы можете сделать что-то вроде 16 + %0
, чтобы получить доступ к области памяти на 16 байт перед операндом памяти.
Как обычно, это помогает посмотреть на вывод asm компилятора.Я поместил ваш код в проводник компилятора Godbolt (с gcc и clang) и удалил статический встроенный материал, чтобы мы могли видеть asm для автономного определения функции.
void*
_os_tsd_get_direct(unsigned long slot)
{
void *ret;
__asm__("mov %%gs:%1, %0\n\t"
"nop # operand 1 was %1" : "=r" (ret) : "m" (*(void **)(slot * sizeof(void *))));
return ret;
}
собирается в
#gcc -O3
mov %gs:0(,%rdi,8), %rax
nop # operand 1 was 0(,%rdi,8)
ret
Я использовал NOP вместо просто комментария, поэтому он все еще виден даже после того, как Godbolt удаляет строки только для комментариев.Часто удобно добавлять фиктивные комментарии, показывающие, какие были операнды шаблона (особенно, если вы когда-либо используете какие-либо инструкции с неявными операндами и хотите посмотреть, что компилятор выбрал для операндов, которые не упомянуты в шаблоне иначе).*
Здесь я добавил это только для того, чтобы указать, что 0(,%rdi,8)
, подставляемый компилятором, - это просто текст, который может идти везде, где вы его просите.Хитрость в том, что мы просим об этом сразу после %%gs:
.
void *ptr = *(void **)(slot * sizeof(void *));
Это делает что-то совершенно другое.Вы фактически разыменовываете смещение TLS как указатель на плоское виртуальное адресное пространство (используя базу сегмента DS по умолчанию = 0).
Если вы хотите разбить его, вы должны сделать
void * separated_os_tsd_get_direct(unsigned long slot) {
void *ret;
unsigned long slot_offset = slot * sizeof(void*);
void **gs_ptr = (void **)slot_offset;
__asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" (*gs_ptr));
return ret;
}
компилируется в:
separated_os_tsd_get_direct(unsigned long):
mov %gs:0(,%rdi,8), %rax
ret
Очень важно, чтобы операнд шаблона asm был разыменованием указателя, а не локальным.При включенной оптимизации локальный объект может быть оптимизирован и превращен обратно в указатель с разыменованием исходного местоположения (если он написан с использованием семантики, которая делает это возможным, в отличие от вашей версии), но лучше убедиться, что это безопасно, избегая фактического разыменования, отличного отв выражении внутри ограничения "m"(*ptr)
.