Pull to refresh

Comments 31

Ещё вместо флага O0 можно попробовать Os — будет сгенерирован максимально короткий код без лишних инструкций.

можете взять связку из актуального gcc, OllyDbg и NASM.

GCC вполне умеет сам сохранять asm код (кажись ключ -S) Диалект по умолчанию AT&T но можно переключить на masm (не помню ключ). В коде тоже можно асм использовать (правда только AT&T вроде)
Ну а визуальный отладчик есть в CodeBlocks (который интегрирован с GCC), что вполне удобно. Под вындой вообще достаточно поставить CodeBlocks и получить сразу всё комплектом.
Если Вы пользуетесь gcc то gcc source.c -O0 -masm=intel -S -o source.s
надеюсь, что продолжение будет.
Потому что уже достаточно случаев, когда заявлялись циклы статей, но после первой/реже второй все останавливалось.
так что, надеюсь, что автор не бросит писать )
Что в этом материале относится к языку Си?
Суть статьи — «постигаем глубже конкретную версию компилятора gcc с конкретными флагами компиляции, используя ассемблер».
если вы назовете компилятор, на котором будет принципиальная разница в коде для этих простейших примеров, я с удовольствием опишу эту разницу в следующей статье)
Чтобы далеко не ходить, возьмите clang или MSVC, и свой первый же пример: переменная a окажется в стеке, несмотря на указание register; а в примере с умножением на два вместо сложения будет сдвиг влево.
Чтобы совсем далеко не ходить, можно зайти на godbolt.org и сравнивать любой компилятор с любым.
И да, рассматривать ассемблер таргета малоэффективно, потому что для другого таргета он будет совсем другим. Более информативно смотреть промежуточный код.
А в примере с делением на два эти три компилятора (gcc, clang, MSVC) выдают три разных реализации; в частности, clang — как раз лобовой idiv.
Только с -O0.
При -O1 и больше имеем:
foo(int): # @foo(int)
  mov eax, edi
  shr eax, 31
  lea eax, [rax + rdi]
  sar eax
  ret
Само собой. Но автору-то
хочется видеть интерпретацию нашего кода в asm, а не оптимизированного.


Подозреваю, что автор считает, что для каждого фрагмента кода на Си есть некая «каноническая» трансляция в машкод x86, и с отключённой оптимизацией все компиляторы будут выдавать примерно одну и ту же «каноническую трансляцию».
Так вот, это не так.
Если честно, я не совсем понимаю смысла изучения ассеблерного кода, кроме тех случаев, когда мы изучаем работу непосредственно компилятора.
В том же clang-е большое число проходов оптимизации, и общее количество изменений, которые они могут внести в код в разных обстоятельствах, достигает астрономических величин.
Может быть интересно сравнивать разные реализации одной и той же функции, чтобы понять, какая из них более оптимальна, но пытаться выявить и запомнить все комбинации ассемблерных команд на выходе компилятора — бесполезное дело, имхо.
Мой комментарий в начале ветки — как раз об этом: что автор постигает конкретный компилятор с конкретными настройками, и выдаёт это за постижение Си :-)

(Если что, в gcc тоже не одна сотня проходов трансляции/оптимизации, с самыми неожиданными взаимосвязями между проходами. Одной из целей создания llvm/clang как раз и было заменить эту кастрюлю спагетти чем-то более удобным в обслуживании.)
Подозреваю, что автор считает, что для каждого фрагмента кода на Си есть некая «каноническая» трансляция в машкод x86, и с отключённой оптимизацией все компиляторы будут выдавать примерно одну и ту же «каноническую трансляцию».

нет, автор так не считает. Но рассматривать все и со всех сторон, еще и в одной статье, просто перебор.
Предлагаю в следующей статье рассматривать не способы удвоения целого числа, а какие-нибудь нетривиальные хитрости, которых наивный программист на Си от компилятора не ожидал бы — например, трансляцию условных выражений (типа x==4?3:2) в код без ветвлений.
Там разница между компиляторами, действительно, будет куда интереснее, чем общие места.
Спасибо за пример, посмотрел, красиво.
И gcc, и clang.
Для этого примера — действительно всюду выходит одинаково.
Интересная разница между компиляторами начинается в примерах навроде x&4?4:2
еще не рассмотрел такой пример:
int a = 1;
int main(void)
{
return a;
}
переменная будет и не регистровая и не стековая
Так же нам нужен декомпилятор

Простите за занудство, но вы используете дизассемблер, а не декомпилятор.
А про то, что можно сразу генерировать ассемблерные листинги при компиляции, выше уже написали.
Буду с нетерпением ждать продолжения. Это головоломно, но интересно.

Надеюсь, хоть какое-то количество программистов благодаря таким статьям узнает, какие вообще соглашения бывают у компиляторов — calling conventions, stack frames, вот это всё. А то люди считают stack trace какой-то магией.


Конечно, лучше изучать на примере своего рабочего компилятора (поэтому странно, что вы 80x86 32 bit за основу взяли, а не 64), но и так пригодится.


P.S. У самого на мониторе наклеена бумажка с перечнем регистров в Objective C. При отладке в глубинах библиотечных функций — очень выручает. Но Objective C тут попроще обычного — есть полноценная рефлексия и т.п.

Только не забыть упомянуть, что «calling conventions, stack frames, вот это всё» (собирательно называемое ABI) различаются в зависимости от ОС, процессора и т.д.; даже на x86 и на x64 они сильно разные.
А то новичок из этого материала мог получить впечатление, что единственная разница между ними двумя — это «меньше регистров, больше внимания к сути».
push ebp
mov ebp, esp

push ebx
mov ebx, 1
mov eax, ebx

pop ebx
pop ebp
ret
Первые две строчки соответствую прологу функции, и мы их разберем в статье о функциях.

Все-таки сохранение на стек волатильных регистров (ebx) является частью пролога. Его восстановление вы отнесли к эпилогу.

да, я зря не уточнил, что 3-яя тоже относится. Спасибо, я поправил это место.
Слово — это 16 бит.

Стоило сделать ремарку что размер слова рознится в зависимости от платформы.
И исторических привычек для этой платформы. Например для 32/64 битных x86 общепринято слово 16 бит, хотя оно 32 или 64 соответственно.
В тексте об этом сказано явно:
Термин получил распространение в эпоху 16-ти битных процессоров, тогда в регистр помещалось ровно 16 бит. Такой объем информации стали называть словом (word).

Но действительно стоило упомянуть, что на других процессорах традиции другие. Например, на ARM (даже 64-битных) словом считаются 32 бита.
В принципе это все становится понятно за пару дней работы с отладчиком. Можно еще наоборот делать — переводить ассемблерный код в эквивалентный С-код, основываясь на действиях инструкций. Сначала напрямую, с goto и метками, потом думать, как это объединить в циклы и условия. C-код компактнее, так удобнее разбираться, что делает программа к которой нет исходников.
Предлагаю параллельно с примерами на чистом C рассматривать и аналогичные по функциональности примеры на C++. Очень наглядно демонстрирует, что вменяемые компиляторы (тот же GCC) делают для них совершенно идентичный код (как и должно быть, собственно).

Удивительно, но многие до сих пор считают, что программа на чистом C в общем случае дает более легкий и быстрый код, нежели функционально эквивалентная ей программа на C++. :)
Sign up to leave a comment.

Articles