Использует ли [ebp * 2] DS или SS сегмент?

IDM говорит, что память op использует сегмент SS, если EBP используется в качестве базового регистра. В результате [ebp + esi] и [esi + ebp] ссылаются на сегменты SS и DS соответственно. См. Документ NASM: 3.3. Эффективный адрес.

В том же разделе NASM упомянул, как создать более короткий машинный код, заменив [eax*2] на [eax+eax].

Однако NASM также генерирует [ebp + ebp] для [ebp*2] (т.е. Нет базового регистра).

Я подозреваю, что [ebp+ebp] ссылается на сегмент SS, а [ebp*2] ссылается на сегмент DS.

Я задал NASM этот вопрос. Они думают, что [ebp*2] и [ebp+ebp] одинаковы, но для меня это не имеет смысла. Очевидно, что [ebp+ebp] (ebp в качестве базового регистра) ссылается на сегмент SS. Если они совпадают, [ebp*2 должен ссылаться на SS. Это означает, что SS ссылается до тех пор, пока ebp является базовым или индексным регистром, который, в свою очередь, означает, что оба сегмента [ebp + esi] и [esi + ebp] ссылаются на SS, поэтому они должны быть одинаковыми.

Кто-нибудь знает, какой сегмент [ebp*2] использует?

Ответ 1

Руководство Intel приведено ниже на рисунке 3-11, в котором описывается значение Offset = Base + (Index * Scale) + Displacement:

Использование регистров общего назначения в качестве базовых или индексных компонентов ограничено следующим образом:

  • Регистр ESP не может использоваться как индексный регистр.
  • Когда в качестве базы используется регистр ESP или EBP, сегмент SS является сегментом по умолчанию. Во всех остальных случаях сегмент DS является сегментом по умолчанию.

Это означает, что NASM ошибочно, когда он меняет [ebp*2] на [ebp+ebp] (чтобы избежать 32-битного смещения).

[ebp*2] использует DS потому что ebp не используется в качестве базы
[ebp+ebp] использует SS потому что один из ebp используется как базовый

Тогда было бы лучше указать, что вы не хотите этого поведения из NASM.
До тех пор, пока авторы NASM не поймут свою ошибку, вы можете отключить это поведение (где EBP используется как индекс), написав:

[NoSplit ebp*2]

Ответ 2

Действительно, выбор оптимизации NASM несовместим, предполагая, что ss и ds взаимозаменяемы (т.е. Модель с плоской памятью) при разделении [ebp*2] на [ebp+ebp] чтобы сохранить 3 байта (disp32 vs. disp8), но не оптимизировать [ebp + esi] в [esi + ebp] чтобы избежать disp8.

руководство NASM даже упоминает разный сегмент по умолчанию, что противоречит заключению, которое вы извлекли из неправильной информации, которую вы получили о [0 + ebp*2] против [0+ebp+ebp*1].)

EBP или ESP в качестве базового регистра подразумевают SS, в противном случае по умолчанию используется DS. Когда в режиме адресации NASM используются два регистра, первый из них является базовым, если вы не пишете [ebp*1 + esi], явно применяя масштабный коэффициент к первому. Индексный регистр никогда не предполагает сегмента, что имеет смысл, если вы думаете о намерении проекта: индекс относительно сегмента: смещение, заданное базовым регистром или абсолютным disp32.

Как написано, [ebp*2] - это режим индексированной адресации, неявно требующий 4 байта нулей в виде 32-битных смещений. Вы можете заставить NASM закодировать его таким образом с помощью [nosplit ebp*2].

Возможно, NASM и YASM упустили этот угол, потому что модели с плоской памятью почти универсальны за пределами 16-битного кода. (И режимы 16-разрядной адресации различны и не поддерживают масштабные коэффициенты. Хотя вы можете использовать 32-битные режимы адресации в 16-битном коде, чтобы использовать преимущества масштабных коэффициентов и более широкий выбор регистров, даже в чистом реальном режиме, чем "нереальный" режим, который позволяет устанавливать предельные значения сегментов, достаточные для того, чтобы использовать смещения> 2 ^ 16.)

Все основные 32-разрядные и 64-разрядные операционные системы x86 используют модель с плоской памятью, где SS и DS взаимозаменяемы, что делает эту оптимизацию безопасной для этих ОС, когда вы не делаете ничего странного. Сегментация иногда использовалась для создания неисполняемых стеков до того, как это поддерживалось таблицами страниц, но это все еще модель с плоской памятью. (64-битный код исправляет базовый/предел для CS/DS/ES/SS, поэтому эта оптимизация всегда безопасна там, если SS не является непригодным для использования сегментом целиком, например, возможно, защищенным от записи, если это возможно).

Тем не менее, любое предположение о модели с плоской памятью должно быть необязательным. Это ошибка в NASM и YASM. Они должны либо уважать разницу между SS и DS, либо должны в полной мере использовать плоскую модель памяти, чтобы помочь программистам, которые не помнят, какие режимы адресации имеют "скрытые" дополнительные байты, такие как оптимизация [ebp+esi] без смещение в [esi+ebp]. Предпочтительно должен быть вариант или директива, чтобы сообщить ассемблеру, что он может считать SS и DS одинаковыми.

Операнды в LEA всегда могут воспользоваться, потому что LEA занимается только смещенной частью адреса, поэтому сегменты не имеют значения. (И это было бы наиболее распространенным вариантом использования для режима адресации, такого как [ebp*2] без смещения: использование этого в качестве адреса памяти могло бы эмулировать память, адресуемую по словам? Это просто странно, обычно там есть реальный указатель как один компонент адреса.)


Понимание x86 32/64-разрядных режимов адресации:

Помимо 64-битной относительной адресации RIP, 32/64-битные режимы адресации - это любое подмножество disp0/8/32 + base_reg + idx_reg*1/2/4/8, где каждый из трех терминов/компонентов является необязательным. Но требуется хотя бы один из disp32 или базовый регистр. (См. Также Ссылки на содержимое ячейки памяти (режимы адресации x86)).

[disp32=0 + ebp*2] (с disp32 = ноль) имеет сегмент по умолчанию = DS. Вы можете получить эту кодировку в NASM с [nosplit ebp*2], а адреса, такие как [ebp*4] не могут быть разделены.

[ebp + ebp + disp8=0] имеет сегмент по умолчанию = SS, потому что EBP используется в качестве базового регистра.

Кодировка, которая будет означать ebp без смещения, фактически означает disp32 без базовой рег, поэтому disp32 является фактически базой (подразумевая регистр сегмента DS, поскольку база не является EBP или ESP). Это имеет место с байтом SIB или без него, поэтому [ebp + ebp*1] еще нужно закодировать с помощью disp8 = 0. Другие регистры не имеют этой проблемы, поэтому обычно разделение экономит 4 байта вместо 3 для EBP. (За исключением r13 который использует ту же самую модификацию ModR/M как RBP, я предполагаю, что часть аппаратного декодирования не нуждается в дополнительном бите из префикса REX.)

ESP не может быть индексным регистром, поэтому [esp*2] невозможно кодировать с или без разделения. Таким образом, особый случай оптимизации NASM влияет только на EBP*2. (base = ESP - это код выхода для байта SIB, а index = ESP в байте SIB означает отсутствие индекса, позволяющего кодировать [esp + 12].)

Но, к сожалению, NASM/YASM раскололи EBP*2 даже когда есть константа, которая в любом случае нуждается в disp32, например [symbol + ebp*2], где она не сохраняет никаких байтов и на самом деле вредит производительности для LEA (но не загружает/магазинов) на процессорах семейства Sandybridge. 3-компонентный lea eax, [symbol + ebp + ebp*1] медленнее, чем 2-компонентный lea eax, [symbol + ebp*2]: более высокая латентность и пропускная способность 1-на-час вместо 2. Согласно http://agner.org/optimize/, они будут одинаково медленными на AMD Bulldozer/Ryzen, потому что масштабированный индекс делает "медленный LEA" даже с двумя компонентами.

IDK, если какие-либо старые процессоры лучше работают с немасштабированным индексом и 3-компонентными режимами адресации, для LEA или для реальных операндов памяти.


Поведение NASM и YASM:

 $ nasm -felf32 -g -Fdwarf foo.asm
 $ objdump -drwC -Mintel -S foo.o | sed 's/DWORD PTR//'
 # (edited to put the NASM source line addressing mode onto the same line as the disassembler output, instead of separate lines)
00000000 <sym-0x2c>:
   0:   8b 04 2e                mov    eax, [esi+ebp*1]         ; [esi+ebp]
   3:   8b 44 35 00             mov    eax, [ebp+esi*1+0x0]     ; [ebp + esi]
   7:   8b 04 2e                mov    eax, [esi+ebp*1]         ; [ebp*1 + esi]
   a:   8b 44 2d 00             mov    eax, [ebp+ebp*1+0x0]     ; [ebp*2]
   e:   8b 04 6d 00 00 00 00    mov    eax, [ebp*2+0x0]         ; [nosplit ebp*2]
  15:   8b 45 00                mov    eax, [ebp+0x0]           ; [ebp*1]   ; "split" into base=ebp with no SIB byte
  18:   8b 04 2d 00 00 00 00    mov    eax, [ebp*1+0x0]         ; [nosplit ebp*1]
  1f:   8b 84 2d d2 04 00 00    mov    eax, [ebp+ebp*1+0x4d2]   ; [ebp*2 + 1234]   ; bad split for LEA, neutral on modern CPUs for load/store
  26:   8b 85 15 cd 5b 07       mov    eax, [ebp+0x75bcd15]     ; [ebp*1 + 123456789]
sym:       ; using a symbol reference instead of a numeric constant doesn't change anything
  2c:   8b 84 2d 2c 00 00 00    mov    eax, [ebp+ebp*1+0x2c]    2f: R_386_32    .text   ; [ebp*2 + sym]
  33:   8b 84 2d 2c 00 00 00    mov    eax, [ebp+ebp*1+0x2c]    36: R_386_32    .text   ; [sym + ebp*2]

YASM кодирует все эти случаи идентично NASM.