Поток локального реального использования нижележащих сегментных регистров

Я прочитал ряд статей и ответы S / O, в которых говорилось, что (в linux x86_64) FS (или GS в некоторых вариантах) ссылается на запись таблицы страниц, зависящую от потока, которая затем дает массив указателей на фактические данные, которые доступны для совместного использования. данные. Когда потоки меняются местами, все регистры переключаются, и поэтому изменяется базовая страница с потоками. Доступ к многопоточным переменным осуществляется по имени с помощью всего 1 дополнительного перехода указателя, а значения, на которые имеются ссылки, могут использоваться другими потоками. Все хорошо и правдоподобно.

В самом деле, если вы посмотрите на код для __errno_location(void), функции, стоящей за errno, вы найдете что-то вроде (это от musl, но gnu не сильно отличается):

static inline struct pthread *__pthread_self()
{
    struct pthread *self;
    __asm__ __volatile__ ("mov %%fs:0,%0" : "=r" (self) );
    return self;
}

И из glibc:

=> 0x7ffff6efb4c0 <__errno_location>:   endbr64
   0x7ffff6efb4c4 <__errno_location+4>: mov    0x6add(%rip),%rax        # 0x7ffff6f01fa8
   0x7ffff6efb4cb <__errno_location+11>:        add    %fs:0x0,%rax
   0x7ffff6efb4d4 <__errno_location+20>:        retq

Поэтому я ожидаю, что фактическое значение FS изменится для каждого потока. Например. в отладчике, gdb: info reg или p $fs, я бы увидел, что значение FS будет различным в разных потоках, но нет: ds, es, fs, gs все время равны нулю.

В моем собственном коде я пишу что-то вроде ниже и получаю то же самое - FS неизменен, но TLV работает:

struct Segregs
{
    unsigned short int  cs, ss, ds, es, fs, gs;
    friend std::ostream& operator << (std::ostream& str, const Segregs& sr)
    {
        str << "[cs:" << sr.cs << ",ss:" << sr.ss << ",ds:" << sr.ds
            << ",es:" << sr.es << ",fs:" << sr.fs << ",gs:" << sr.gs << "]";
        return str;
    }
};

Segregs GetSegRegs()
{
    unsigned short int  r_cs, r_ss, r_ds, r_es, r_fs, r_gs;
    __asm__ __volatile__ ("mov %%cs,%0" : "=r" (r_cs) );
    __asm__ __volatile__ ("mov %%ss,%0" : "=r" (r_ss) );
    __asm__ __volatile__ ("mov %%ds,%0" : "=r" (r_ds) );
    __asm__ __volatile__ ("mov %%es,%0" : "=r" (r_es) );
    __asm__ __volatile__ ("mov %%fs,%0" : "=r" (r_fs) );
    __asm__ __volatile__ ("mov %%gs,%0" : "=r" (r_gs) );
    return {r_cs, r_ss, r_ds, r_es, r_fs, r_gs};
}

Но на выходе?

Main: Seg regs : [cs:51,ss:43,ds:0,es:0,fs:0,gs:0]
Main:    tls    @0x7ffff699307c=0
Main:    static @0x96996c=0
 Modified to 1234
Main:    tls    @0x7ffff699307c=1234
Main:    static @0x96996c=1234

 Async thread
[New Thread 0x7ffff695e700 (LWP 3335119)]
Thread: Seg regs : [cs:51,ss:43,ds:0,es:0,fs:0,gs:0]
Thread:  tls    @0x7ffff695e6fc=0
Thread:  static @0x96996c=1234

Так что-то еще происходит на самом деле? Какие дополнительные уловки творится и зачем усложнять?

Для контекста я пытаюсь сделать что-нибудь забавное с вилками, поэтому я хотел бы знать кровавые детали.


person Gem Taylor    schedule 16.12.2020    source источник
comment
GDB может показать вам значения регистров сегмента; вам не нужно писать встроенный asm. Но ваш способ дает хороший компактный результат, который можно опубликовать.   -  person Peter Cordes    schedule 16.12.2020
comment
@PeterCordes Действительно, есть, но я дошел до того, что не доверял этому и хотел убедиться в этом сам :-)   -  person Gem Taylor    schedule 18.12.2020


Ответы (1)


В 64-битном режиме фактическое содержимое 16-битных сегментных регистров FS и GS по существу не имеет значения. Вместо этого в ЦП есть отдельные 64-битные регистры FSBASE и GSBASE, и когда вы указываете, скажем, переопределение сегмента FS для инструкции, базовый адрес из регистра FSBASE добавляется к эффективному адресу операнда для определения фактического линейного адрес для доступа.

Структура контекста ядра для каждого потока хранит копии его регистров FSBASE и GSBASE, и они перезагружаются соответствующим образом при каждом переключении контекста.

Итак, что на самом деле происходит, так это то, что каждый поток устанавливает свой регистр FSBASE так, чтобы он указывал на свое собственное локальное хранилище потока. (В зависимости от функций ЦП и конструкции ОС это может быть возможно только для привилегированного кода, поэтому может потребоваться системный вызов.) Затем инструкции с переопределением сегмента FS могут использоваться для доступа к объекту с заданным смещением в потоке. блок локального хранилища, как вы видели.

С другой стороны, в 32-битном режиме значения в FS и GS имеют значение; они являются селекторами сегментов, которые используются для индексации таблицы дескрипторов, поддерживаемой ядром. Таблица дескрипторов содержит фактическую информацию о сегменте, включая его базовый адрес, и вы можете использовать системный вызов, чтобы попросить ядро ​​изменить его. Каждый поток будет иметь свою собственную локальную таблицу дескрипторов, поэтому вы не обязательно увидите разные селекторы в FS для разных потоков, но все равно будет тот случай, когда инструкции переопределения FS из разных потоков приведут к доступу к разным линейным адресам.

person Nate Eldredge    schedule 16.12.2020
comment
Ах, в этом есть смысл! Я думаю, что все же стоит сказать, что это превышает предел подсчета 64 КБ, который есть у сегментных регистров. Думаю, для 32-битного режима адекватным считалось 64К потоков, но не для 64-битного. - person Gem Taylor; 16.12.2020
comment
@GemTaylor: Я предполагаю, что ограничение в 64 КБ в 32-битном режиме будет зависеть от реализации потоковой передачи: был ли отдельный LDT для каждого потока или только один для каждого процесса. Если бы вы выполняли легкие потоки в пользовательском пространстве, возможно, у вас был бы только один LDT, и вам понадобился бы другой селектор для каждого потока. Таким образом, вам нужно только перезагрузить FS при переключении потоков. Для потоков, поддерживаемых ядром, у вас может быть LDT для каждого потока; не было бы необходимости в уникальных селекторах, поэтому вы никогда не закончите. - person Nate Eldredge; 16.12.2020
comment
@GemTaylor: для 386 - да (или используйте LDT). Но современные 32-битные ядра используют тот же механизм, что и 64-битные ядра, для чтения или записи FS.base или GS.base: rdmsr и wrmsr с использованием номера MSR C000_0100h для FS (MSR_FS_BASE) или даже на более новых процессорах _4 _ / _ 5_, которые даже позволяет ядру разрешать пользовательскому пространству изменять базы сегментов. (Когда ЦП находится в режиме ядра, может ли он читать и писать в любой регистр?). - person Peter Cordes; 16.12.2020
comment
Унаследованный механизм фактической записи FS или GS для запуска чтения GDT или LDT, я думаю, работает в 64- или 32-битном режиме, но его избегают в 32-битном режиме, потому что он медленнее и не может считывать текущие базы для сохранения переключение контекста. (И может установить только 32-битную базу, поэтому не может полностью использоваться для 64-битных ядер. MSR гарантированно доступны для x86-64, поэтому 64-битное ядро ​​не нуждается в резервном варианте для совместимости с 386). По крайней мере, это Насколько я понимаю, но я почти уверен, что 32-битное ядро ​​Linux по-прежнему будет иметь одинаковое значение FS для каждого потока. Не только 32-битное пространство пользователя под 64 (режим совместимости) - person Peter Cordes; 16.12.2020
comment
И, кстати, FS и GS действительно имеют значение в 64-битном режиме. 64-битный код может аварийно завершить работу (вызвав исключение), переместив неверное значение в FS или GS. Но копирование DS в FS не приводит к сбою, потому что это действительный селектор. Попробуйте это с mov eax, ds / mov fs, eax / mov edx, 12345 / mov fs, edx, созданным как 64-битный статический исполняемый файл для Linux. (НАСМ и Л.Д.). Segfault на mov fs, edx, но не на mov fs, eax. Обычно 64-битное ядро ​​оставляет им нулевое значение селектора (0), потому что оно может (в длинном режиме и режиме совместимости), а не потому, что никакое другое значение не имеет смысла. - person Peter Cordes; 16.12.2020
comment
Спасибо! Я показал в быстром тесте, что в принципе копирование этого базового значения FS с помощью arch_prctl даст мне нужное мне поведение. - person Gem Taylor; 18.12.2020