Встроенная сборка Gcc: что не так с динамическим выделенным регистром `r` в входном операнде?

Когда я тестирую встроенную сборку GCC, я использую функцию test для отображения символа на экране с эмулятором BOCHS. Этот код работает в 32-битном защищенном режиме. Код выглядит следующим образом:

test() {
    char ch = 'B';
    __asm__ ("mov $0x10, %%ax\n\t" 
                "mov %%ax, %%es\n\t"
                "movl $0xb8000, %%ebx\n\t"
                "mov $0x04, %%ah\n\t" 
                "mov %0, %%al\n\t" 
                "mov %%ax, %%es: ((80 * 3 + 40) * 2)(%%ebx)\n\t" 
                ::"r"(ch):);
}

В результате я получаю: введите описание изображения здесь

Красный символ на экране не отображает B правильно. Однако, когда я изменил входной регистр r на c следующим образом: ::"c"(ch):);, который является последней строкой вышеуказанного кода, символ "B" отображается нормально:

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

Я проследил код сборки, я обнаружил, что код был собран в mov al, al, когда выбран регистр r, а значение ax равно 0x0010, поэтому al - 0x10. Результат должен быть таким, но почему он выбрал регистр al. Разве он не должен выбирать регистр, который раньше не использовался? Когда я добавляю список clobbers, я решил проблему.

Ответ 1

Как и комментарий @MichaelPetch, вы можете использовать 32-битные адреса для доступа к любой памяти, которую вы хотите от C. Выпуски asm gcc будут принимать плоское пространство памяти и предположить, что он может скопировать esp в edi и использовать rep stos к нулю, например, к некоторой памяти стека (для этого требуется, чтобы %es имела ту же базу, что и %ss).

Я бы предположил, что лучшим решением является не использование какого-либо встроенного asm, а вместо этого просто используйте глобальную константу в качестве указателя на char. например.

// pointer is constant, but points to non-const memory
uint16_t *const vga_base = (uint16_t*)0xb8000;   // + whatever was in your segment

// offsets are scaled by 2.  Do some casting if you want the address math to treat offsets as byte offsets
void store_in_flat_memory(unsigned char c, uint32_t offset) {
  vga_base[offset] = 0x0400U | c;            // it matters that c is unsigned, so it zero-extends instead of sign-extending
}
    movzbl  4(%esp), %eax       # c, c
    movl    8(%esp), %edx       # offset, offset
    orb     $4, %ah   #, tmp95         # Super-weird, wtf gcc.  We get this even for -mtune=core2, where it causes a partial-register stall
    movw    %ax, 753664(%edx,%edx)  # tmp95, *_3   # the addressing mode scales the offset by two (sizeof(uint16_t)), by using it as base and index
    ret

Из gcc6.1 на godbolt (ссылка ниже), с -O3 -m32.

Без const код, подобный vga_base[10] = 0x4 << 8 | 'A';, должен был бы загрузить глобальный файл vga_base, а затем смещаться от него. С const, &vga_base[10] является константой времени компиляции.


Если вам действительно нужен сегмент:

Поскольку вы не можете оставить %es измененным, вам нужно сохранить/восстановить его. Это еще одна причина, чтобы избежать ее использования в первую очередь. Если вам действительно нужен специальный сегмент для чего-то, настройте %fs или %gs один раз и оставьте их установленным, поэтому он не влияет на нормальную работу любых инструкций, которые не используют переопределение сегмента.

Существует встроенный синтаксис для использования %fs или %gs без встроенного asm, для локальных переменных потока. Возможно, вы сможете воспользоваться из него, чтобы избежать встроенного asm вообще

Если вы используете пользовательский сегмент, вы можете сделать его базовым адресом ненулевым, поэтому вам не нужно добавлять 0xb8000 самостоятельно. Тем не менее, процессоры Intel оптимизируются для работы с плоской памятью, поэтому генерация адресов с использованием ненулевых сегментов базируется на нескольких циклах медленнее, IIRC.

Я нашел запрос для gcc, чтобы разрешить переопределения сегмента без встроенного asm, и вопрос о добавлении поддержки сегмента в gcc. В настоящее время вы не можете этого сделать.


Выполнение вручную в asm с выделенным сегментом

Чтобы посмотреть на выход asm, я положил его на Godbolt с -mx32 ABI, поэтому аргументы передаются в регистрах, но адреса не должны быть расширены до 64 бит. (Я хотел избежать шума загрузки args из стека для кода -m32. Asm для защищенного режима будет похож)

void store_in_special_segment(unsigned char c, uint32_t offset) {
    char *base = (char*)0xb8000;               // sizeof(char) = 1, so address math isn't scaled by anything

    // let the compiler do the address math at compile time, instead of forcing one 32bit constant into a register, and another into a disp32
    char *dst = base+offset;               // not a real address, because it relative to a special segment.  We're using a C pointer so gcc can take advantage of whatever addressing mode it wants.
    uint16_t val = (uint32_t)c | 0x0400U;  // it matters that c is unsigned, so it zero-extends

    asm volatile ("movw  %[val], %%fs: %[dest]\n"
         : 
         : [val] "ri" (val),  // register or immediate
           [dest] "m" (*dst)
         : "memory"   // we write to something that isn't an output operand
    );
}
    movzbl  %dil, %edi        # dil is the low 8 of %edi (AMD64-only, but 32bit code prob. wouldn't put a char there in the first place)
    orw     $1024, %di        #, val   # gcc causes an LCP stall, even with -mtune=haswell, and with gcc 6.1
    movw  %di, %fs: 753664(%esi)    # val, *dst_2

void test_const_args(void) {
    uint32_t offset = (80 * 3 + 40) * 2;
    store_in_special_segment('B', offset);
}
    movw  $1090, %fs: 754224        #, MEM[(char *)754224B]

void test_const_offset(char ch) {
    uint32_t offset = (80 * 3 + 40) * 2;
    store_in_special_segment(ch, offset);
}
    movzbl  %dil, %edi  # ch, ch
    orw     $1024, %di        #, val
    movw  %di, %fs: 754224  # val, MEM[(char *)754224B]

void test_const_char(uint32_t offset) {
    store_in_special_segment('B', offset);
}
    movw  $1090, %fs: 753664(%edi)  #, *dst_4

Таким образом, этот код получает gcc, чтобы отлично справляться с использованием режима адресации для выполнения математики адреса и делать как можно больше во время компиляции.


Регистр сегментов

Если вы хотите изменить регистр сегментов для каждого магазина, имейте в виду, что он медленный: Agner Fog insn tables stop include mov sr, r после Nehalem, но на Nehalem это инструкция 6 uop, которая включает в себя 3 загрузки uops (из GDT, которые я предполагаю). Он имеет пропускную способность один на 13 циклов. Чтение сегментного регистра является точным (например, push sr или mov r, sr). pop sr даже немного медленнее.

Я даже не собираюсь писать код для этого, потому что это такая плохая идея. Убедитесь, что вы используете ограничения clobber, чтобы компилятор знал о каждом зарегистрированном вами регистре, или у вас будут жесткие ошибки отладки, когда окружающий код перестает работать.

Смотрите x86 теги wiki для GNU C inline asm info.