2014 dxdy logo

Научный форум dxdy

Математика, Физика, Computer Science, Machine Learning, LaTeX, Механика и Техника, Химия,
Биология и Медицина, Экономика и Финансовая Математика, Гуманитарные науки




Начать новую тему Ответить на тему На страницу Пред.  1 ... 3, 4, 5, 6, 7, 8, 9  След.

Нужна ли такая тема про ассемблер?
Опрос закончился 24.01.2024, 03:22
Да, почитаю. 50%  50%  [ 12 ]
Да, поспрашиваю. 25%  25%  [ 6 ]
Да, поотвечаю. 4%  4%  [ 1 ]
Мне всё равно, но не против, дерзайте. 17%  17%  [ 4 ]
Нет, не интересно, полно готовой литературы. 0%  0%  [ 0 ]
Нет, ничего в этом не понимаю и не собираюсь. 0%  0%  [ 0 ]
Нет, форум не про это, есть другие более подходящие. 4%  4%  [ 1 ]
Другое ... 0%  0%  [ 0 ]
Всего голосов : 24
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение19.01.2024, 11:13 
Аватара пользователя


29/04/13
7255
Богородский
Dmitriy40 в сообщении #1626453 писал(а):
неплохое задание на дом: добавить в isprime проверку что текущий делитель стал больше корня из тестируемого числа и не лопатить делители до конца таблицы.
Корень вычислять не будем, изврат это, использовать 32-разрядную команду MUL. Проверку добавить перед делением.

Вроде бы вот здесь указано, что надо лопатить делители до конца таблицы:

cmp ESI,max_prime/2

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

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение19.01.2024, 14:44 
Заслуженный участник


20/08/14
11210
Россия, Москва
Yadryara
Пробуйте! Это интересный личный опыт.
Сразу скажу, там есть подвох, правда не знаю когда точно он вылезет, это зависит как именно сделаете проверку. Так что проверяйте внимательнее результаты.
Намёк: не забудьте что в ESI не само число, а его индекс в битовом массиве, где только нечётные. И в программе есть место где есть и само число в прямом виде (но конечно можно его и не использовать).
Ну и совет, он в общем достаточно общий: когда добавляете новое условие старайтесь сохранить все старые, чтобы ничего не попортить. Заработает - можно будет посмотреть какие куски дублируют друг друга и попробовать удалить (закомментировать) - и обязательно перетестировать. Критерий $\pi(x)$ достаточно хорош для $x$ больше (хотя бы в пару раз) размера таблицы, отловит практически любую ошибку. Хотя числа около конца диапазона (и около конца таблицы с обеих сторон от него) лучше проверять дополнительно.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение19.01.2024, 14:58 
Аватара пользователя


29/04/13
7255
Богородский
Dmitriy40 в сообщении #1626495 писал(а):
Так что проверяйте внимательнее результаты.

Так это Вы Гуру, а я пока полуслепой котёнок. Я даже не понимаю толком как те или иные числа печатать хотя бы на экран.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение19.01.2024, 15:18 
Заслуженный участник


20/08/14
11210
Россия, Москва
А пока добавлю ещё интересный момент про функцию SkipFileName, в коде это плохо видно. Как один из вариантов можно представить что она реализует автомат состояний (или конечный автомат) с изначально одним состоянием <текущий символ не пробел> и увеличивающий указатель при каждом попадании в это состояние. Это цикл с меткой .skip (но без проверки на символ кавычки). Потому что параметры запуска отделяются от имени программы (с путём) именно пробелом (может и не одним, но это частности). И раз хотим получить параметры надо пропустить всё до первого пробела. Первое что приходит в голову - конечно же цикл. Но какому-нибудь теоретику первым может придти в голову именно конечный автомат. И программист должен знать про такую интересную структуру управления. И уметь применять когда с ним проще.
При реальной работе как-то вдруг оказалось что в пути (да и в имени программы) бывают пробелы ... И программа засбоила, ведь винда такие строки заключает в кавычки и оставляет пробел внутри строки, его и находит цикл, но это ещё не параметры ... Пришлось добавлять в конечный автомат второе состояние <находимся внутри закавыченной строки> и выход из него делать не по пробелу, а по символу кавычки. А вход из первого состояния - тоже по символу кавычки.
И тут тонкость: конечный автомат можно реализовывать несколькими способами (и программисту тоже это надо знать и уметь пользоваться): или завести переменную с номером состояния и обрабатывать любые состояния в одном общем куске кода; или завести таблицу переходов между состояниями на каждый входной сигнал (появление пробела, кавычки, конца строки); или разделить состояния физически, реализовав каждое отдельным куском кода и тогда переход между состояниями выполняется командами jump (условными или безусловной) и ни переменная ни таблица не нужна (но неявно превращаются в кучу кода). Здесь я применил последний подход, второе состояние реализовано циклом .quotes. А мог бы сделать всё в одном цикле продвижения указателя, проверяя то на одно, то на другое, то на третье, в зависимости от чего-нибудь там.
Аналогично можно воспринимать и GetNumber, тоже сначала состояние <текущий символ пробел> и условия повтора и выхода из него, и второе <текущий символ цифра> и другие условия повтора и выходов из него.

Я к чему. К тому что один и тот же код можно воспринимать как реализацию очень разных более высокоуровневых структур управления: или тупо циклов, или конечного автомата, операций на графами (очень надеюсь до такого не дойдёт), или ещё как. И знание "стандартных" структур сильно помогает в реализации кода (вот такая развёртка конечного автомата оказалась проще и понятнее одного сложного цикла). И да, в данном случае я как раз держал в уме что пишу автомат состояний, слишком уж он "по родному" ложится на задачу поиска первого пробела вне закавыченных строк. И кстати на много других не столь тривиальных задач.
Это не совсем про оптимизацию по скорости, скорее вообще по технике написания кода, собственно на любом языке. Надо знать не только сам язык, но и кучу бэкграунда, общих вещей и понятий, и уметь их применять. И такое знание помогает писать понятный код, не быстрый, но понятный. И только когда он заработал, можно приступать к его усложнению и оптимизации (к сожалению практически всегда оптимизация приводит к усложнению и запутыванию). Я может не в первый (и не пятый) раз это повторяю, но это важно, оптимизация не сводится к изучению всех команд и выбору лучшей их комбинации, часто приходится менять и алгоритм (и структуры данных) ради применения в них других (комбинаций) команд.

-- 19.01.2024, 15:41 --

Yadryara в сообщении #1626502 писал(а):
Я даже не понимаю толком как те или иные числа печатать хотя бы на экран.
Это несложно: в "заклинании" ниже указываем вместо ECX где у нас лежит число и вставляем это "заклинание" где нам надо и радуемся жизни:
Используется синтаксис ASM
                pushad  ;Нужно только если код вставляем куда-то в середину своей программы, где нельзя портить регистры
                invoke  wsprintf, buf, .fmt99, ECX
                invoke  WriteFile, dword [hOut], buf, EAX, temp, 0
                popad   ;Нужно только если код вставляем куда-то в середину своей программы, где нельзя портить регистры
...
.fmt99:         db      '%u, ',0        ;Это кладём вместе с остальными строками форматов после процедуры куда вставляем
Со строкой должно быть всё понятно, обычный формат для printf из C или PARI/GP. Лишь не забыть что любая строка должна завершаться символом 0x00. А перевод строки я предпочитаю указывать явно как 13,10 (0x0D,0x0A). Ну что строки в одинарных кавычках вместо двойных и так понятно по примерам.
Разумеется если надо несколько разных строк формата, то каждой даём свою метку и их и указываем в wsprintf.

На самом деле это конечно не заклинание. Сначала мы дёргаем из WinAPI функцию wsprintf, которая почти как printf, только пишет не на экран, а в строку (байтовый буфер где-то в памяти данных), передаваемую указателем в первом параметре. Во втором параметре указатель на строку формата. А потом список чего хотим вывести. Если хотим не из регистра, а из памяти, то вместо имени регистра указываем dword [адрес] (или byte/word) (как для WriteFile). Про методы адресации говорил выше. Функция wsprintf в EAX вернёт длину сгенерированной строки по переданному ей указателю, далее дёргаем WinAPI для записи в файл с хэндлом (не помню как это на русском) по указателю hOut куда мы в самом начале сохранили хэндл консоли, передаём что за буфер байтов хотим вывести (указатель на buf), сколько байтов из него вывести (лежит в EAX от wsprintf), остальные два параметра служебные (сам не помню, смотреть в доках MS). И она нам выводит в консоль (или любой другой файл или устройство, главное хэндл правильный подать) буфер байтов с сгенерированной строкой.
На уровне идеи - всё. Как оно внутреннее реализуется дело десятое (и буду писать вот прям щас, про вызов функций), главное работает.

-- 19.01.2024, 15:52 --

И не бойтесь пробовать! Ничего страшного не произойдёт (но зуб не дам). При написании программ 99% запусков завершаются вылетом по ошибке (почти всегда непонятной) в винду. Есть негласное правило программистов: если программа заработала правильно с первого раза - ищите страшнейшую ошибку, она замаскировалась, но точно есть! Смешно, но обычно правда. И у меня тоже разумеется. Ну может 98% или даже 95% ... ;-)

Пока мы не пишем программы активно работающие с файлами и дисками и сетью - ничего особо страшного не произойдёт (хотя гарантию никто и не даёт). Максимум - файл лога займёт гигабайты вплоть до всего места на диске (за секунды больше просто физически не записать). Но просто если знаете что программа должна завершиться за пару секунд (а так и должно быть для любых не до конца отлаженных программ, ради этого кардинально уменьшаются все структуры и укорачиваются циклы, временно, пока не заработает), а она продолжает работать уже 5с - обрывайте (Ctrl-C или крестиком в винде) и разбирайтесь что не так. Если не знаете сколько она должна работать - запускайте с меньшими параметрами, вплоть до нулевых, чтобы за полсекунды справилась, и потом плавно увеличивайте, и смотрите порядок роста (выше квадратичного бывает нечасто, зато квадрат как раз очень часто).

-- 19.01.2024, 16:10 --

Yadryara
Про проверку результатов я имел в виду что как раз специально же сделал указание чисел при запуске, не в исходнике. Вот их и указывайте, разные, квадраты простых, не квадраты, малые, большие, около конца таблицы с обеих сторон, около конца диапазона, произведение двух-трёх каких-нибудь красивых (на свой вкус) простых, первые числа (перечислить все до 20 например), или штук 10 подряд после миллиарда, или ... Включите фантазию (или генератор случайных чисел) и вперёд. Чем больше разных тестов - тем выше вероятность обнаружить ошибку. Вдруг она там есть и я тоже лоханулся ... Ну а проверка по $\pi(x)$ обнаружит 99% ошибок, я ж не только ради тормозов её сделал.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение19.01.2024, 16:32 
Заслуженный участник


20/08/14
11210
Россия, Москва
Я выше везде смешиваю понятия указатель и адрес. Ну в общем это одно и то же. Адрес конечно более широкое/общее понятие, но в контексте языка ассемблера их можно считать синонимами. И хоть слово "указатель" длиннее, предпочитаю его чтобы подчеркнуть смысл адреса, что он адресует вполне конкретный объект (например строку в виде массива байтов или двойное слово). Кроме того к понятию указатель прилипает понятие его размера, в x32 ОС это обычно 32 бита (исключения замнём), и он (указатель) всегда помещается в регистр. Как обычное целое число, коим он и является, смыслом его наделяет программист у себя в голове и в момент использования в командах как адреса чего-то там. Процессор не знает что какое-то число в регистре это адрес/указатель (про исключения пока молчим) пока мы не использовали его для адресации (ячейки памяти или бита в регистре). И после этого тоже не знает. Только в момент использования. И можно разделить указатель на число пи или возвести в квадрат или сложить два указателя, процессор выполнит что скажете, сколь бы бессмысленным это не было. Это отличие от ЯВУ, там всё строго - ради уменьшения человеческих ошибок (и смысловых и опечаток).

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение19.01.2024, 21:42 
Заслуженный участник


20/08/14
11210
Россия, Москва
Итак, процедуры и функции.
Всё это есть в книжках, но сильно размазано и может "взгляд с другой стороны" (ну или на меня напала мания графоманства, тоже повод).

Сначала чуть истории. Когда компы были большими, а программы маленькими и в кодах (просто массив чисел), то поначалу всё было хорошо и без функций, пиши себе чиселки и пиши. И даже когда надоело запоминать числа и придумали язык ассемблера (поначалу просто как синонимы числам) всё было неплохо. Но программы росли, функционал ширился, и быстро оказалось что практически одинаковый код приходилось писать много раз в разных частях программы (например преобразование числа в строку или строки в число), а код был уже немаленький (в отличие от памяти которой были считанные килобайты) и много раз дублировать десятки/сотни строк/команд неудобно (и непрактично, вдруг где ошибка или что подправить/улучшить надо - ищи меняй по всему тексту). Решение разумеется все знали ещё со школы - функции в математике, сколько надо раз столько и используй, где и как угодно. Оставалось его реализовать. Сначала сделали чисто программно, на том что было. Это можно и сейчас сделать, на любом процессоре, но что-то лень выдумывать пример. Штука оказалась мегаудобной, памяти кода экономила просто жуть как, а времена были доисторические (до микропроцессоров), каждая ЭВМ имела свою систему команд и добавить в следующую пару-тройку нужных команд вообще ни разу не проблема. И вообще эпоха была ... интересной, каждое удачное/полезное решение могли и делали в виде новой команды. И разумеется работу с функциями. И сделали. Потом пришли микропроцессоры и унаследовали и этот механизм тоже.

Пару слов об отличии функции от процедуры. В математике понятия процедуры нет (точнее там оно совсем из другой оперы, если не ошибаюсь, математику я знаю хуже программирования). В ЯВУ (языках высокого уровня если кто так и не догадался) разделение очень чёткое и формальное: функция возвращает единственный результат (может быть сложным типом данных), процедура ничего не возвращает. Точка. Будет результат потом использоваться, не будет - дело десятое (вызывающей программы и ошибок программиста). В ассемблере и для процессора понятия функции нет - для них функция это та же самая процедура, ничего никуда не возвращающая. Тут можно возмутиться "как же так, вон GetTickCount возвращает же в EAX число - значит функция!". Возвращает. Однако и WriteFile тоже что-то там возвращает (подробности как обычно у MS)! А вызов идентичен (ну параметров больше, не в том дело). И если не смотреть ниже по коду, то отличий нет никаких. Ни в записи ассемблерного текста, ни, как покажу ниже, в машинном коде для процессора. Потому дальше (да и раньше) могу путать функции с процедурами и наоборот ... Отличия лишь в голове программиста, пользуется он изменениями состояния компа (регистров в процессоре, ячеек памяти, файлов на диске, пикселей на экране) или не пользуется.

Хорошо, процедуры. Это выделенный кусок кода, который можно вызывать из разных мест и после его выполнения возвращаться обратно в точку откуда вызвали. Нужно это страшно часто, потому в процессоре давно предусмотрены команды для этого. Вызвать процедуру (вообще говоря произвольный кусок кода, за исключением последней команды) можно командой call с указанием метки начала процедуры. Когда процедура сделает всё что надо и половину чего не надо она должна вернуться откуда вызвали - для этого команда ret (в ЯВУ часто return). Но для этого надо помнить откуда её собственно вызвали, ведь это не одно единственное место куда можно сделать jmp, они могут быть каждый раз разными. Для запоминания места возврата придумано несколько механизмов, но в x86 (и наследнице x64) используется один (и не самый лучших): запоминается в памяти. Где в памяти, она же большая? А вот есть специальный регистр SP/ESP/RSP, и указывающий где в памяти лежит адрес куда надо вернуться.
Т.е. происходит следующее. Команда call уменьшает регистр ESP на 4 (мы же помним что все адреса=указатели равны 32 битам), пишет по новому адресу из ESP адрес следующей команды (куда надо будет вернуться), и делает jmp на указанную метку (в процедуру). Процедура приходит на команду ret, которая читает адрес куда надо сделать jmp из памяти по адресу ESP, увеличивает ESP на 4 (адрес возврата больше не нужен) и делает jmp на прочитанный адрес, тем самым возвращаясь ровно на следующую команду после call. Вуаля.
Именно поэтому регистр ESP хоть и называется регистром общего назначения, но у него очень специальная функция! И использовать его по другому нельзя. (Иногда, очень и очень редко, можно, но это такие дебри, что лучше не полезу, нельзя и всё.)

Замечу, я пока ни слова не сказал как процессор видит параметры процедур и как он возвращает результат функций. Почему? Да потому что процессор об этом вообще ничего не знает! Вот не знает! Ему хватает команд call и ret, всё остальное забота программиста! Рады? То ли ещё будет.
Ещё чуть альтернативной истории. Поначалу всё так и было, каждый программист писал код передачи параметров и возврата результата как ему бог на душу положит, да ещё и в каждой программе по разному (если не ленился), да и в одной программе для разных процедур по разному. (Я кстати иногда и до сих пор так пишу ... Потому что красивее! И короче и понятнее. Иногда. Ну да ладно.) Потом программисты естественно обленились и стали копировать придуманный удобный способ из программы в программу. А потом и между собой, такая стихийная стандартизация. А потом пришёл MS и всё порушил. Пришёл, но не порушил. Просто стали пользоваться одинаковым методом для всех функций и процедур в винде (в дос кстати сильно по другому). И этот способ разумеется поддержали в компиляторах ЯВУ (им же тоже надо обращаться к ОС) и назвали стандартным. И чтобы указать на него стали указывать термин stdcall, т.е. стандартный call. Я могу сильно привирать в последовательности и причинности событий, но результат именно такой.

Итак, как же передаются параметры в процедуры при вызове функций WinAPI (и соответственно любых других, для единообразия). Параметры у каждой функции свои, потому их нельзя (точнее очень неудобно, можно-то всё) хранить в фиксированной памяти. И нельзя хранить в куче (heap) - слишком долго получать оттуда кусок памяти и возвращать при выходе. В регистрах тоже неудобно - они для другого обычно нужны, а их и так мало. Решение лежало на поверхности: есть же стек (место адресуемое ESP и хранящее адреса возвратов из функций), давайте вместе с адресом положим и параметры, они больше никому мешать не будут, и это достаточно быстро. Ура. Тем более что уже есть специальная команда push положить в стек регистр или ячейку памяти или константу, как раз параметр.

Итого вызов процедуры из одной строки на языке ассемблера превращается в такую последовательность команд:
Используется синтаксис ASM
;Было в тексте:
        invoke  wsprintf, buf, .fmt3, ECX, EDX
;Станет в коде и увидит процессор:
        push    EDX
        push    ECX
        push    .fmt3   ;Адрес метки .fmt3
        push    buf     ;Адрес метки buf
        call    dword [wsprintf]        ;Адрес для перехода берётся из ячейки с меткой wsprintf
Что здесь интересного: параметры в стек заталкиваются в обратном порядке, начиная с конца. Не уверен с чем это связано, просто вот так.
Ещё почему-то адрес не указан прямо в call, а лежит где-то в памяти ... Это потому что мы не знаем где реально лежит код функции WinAPI и узнаем это только когда наша программа из файла .exe загрузится виндой в память и тогда винда положит в ячейку wsprintf правильный адрес этой функции WinAPI в какой-то левой DLL из дистрибутива винды. Это частности.
Главное теперь видно куда и как засовываются указанные нами параметры и видим правильную команду процессора call вместо неизвестной ему invoke. Это преобразование делает FASM. Для удобства программиста, ведь согласитесь первая одна строка понятнее 5-ти последних, да ещё про порядок не забыть ...

Но в коде есть и stdcall (не как ключевое слово, а как команда), что с ним. С ним всё хорошо, он отличается от invoke только лишь тем в команде call будет не dword [wsprintf], а прямо адрес скажем isprime, без скобочек [], ведь его мы (в смысле компилятора асма) точно знаем и можем сразу подставить правильный, не дожидаясь загрузки файла виндой в память и не читая из памяти. Весь остальной код ровно такой же. Надеюсь с вызовом процедур понятно.
А с функциями как и говорил, процессору без разницы. Просто в винде принято возвращать результат в EAX (целочисленный, какой бы смысл у него ни был). Ну вот принято. Хотите - в своих делайте как угодно, а MS для WinAPI уже сделала так. И чтобы не путаться через месяц/год/десятилетие лучше делайте так же, единообразно. Я опять же бывает нарушая это правило, но всегда в первых строках функции в комментариях указываю что где принимает и что где возвращает. Это вообще хорошая практика, в примерах выше поленился и размер экономил.

Хорошо, вызывать процедуры научились. А что с самой процедурой, там код и её текст для FASM как-то отличается от просто куска кода? Да, отличается, и это важно.
Формально отличие только одно: в процессе выполнения управление должно попасть на одну из команд ret (да, теоретически их может быть много в одной функции, но так делать очень не стоит, дальше покажу почему). Всё, со стороны процессора больше никаких требований нет. И можно так и писать. Но разумеется для блага всего прогрессивного человечества программиста компилятор доработали и несколько формализовали: процедуры/функции, вызываемые командами invoke/stdcall, начинаются с команды proc (от procedure) и заканчиваются командой endp (end procedure). Это команды компилятору, не процессору. Команда proc имеет параметры, первым идёт имя процедуры (метка, которую указывать в командах call), без этого никуда, иначе как же эту процедуру вызывать. Дальше через запятую нужно указать параметры (если они есть) - тогда FASM сможет сам проверить что при всех миллионах вызовах по всему тексту вы нигде не ошиблись и поставили правильное количество параметров (аргументов). Борьба с опечатками и ошибками.
Можно аргументы и не указывать (ни при объявлении функции командой proc, ни при вызове командой stdcall) и самому их передавать как сможете. И тогда даже вместо stdcall писать обычный call. Можно. Вопрос нужно ли. (Иногда нужно, но это исключение.) Проще, удобнее и надёжнее (я ещё не надоел этими мантрами? но это суровая правда, политая в том числе и кровью) пользоваться уже реализованным механизмом.

Итак, с вызовом разобрались, с передачей параметров тоже. Теперь бы ещё их прочитать в самой процедуре ...
Разумеется FASM спешит на помощь, пишем просто чтение аргумента из памяти (а мы ещё помним что он лежит где-то в стеке вместе с ненужным нам сейчас адресом возврата) например в ECX командой mov ECX,[num] в isprime. FASM сам найдёт в списке аргументов имя num, подсчитает как высоко в стеке от ESP (т-с-с-с, см. ниже) он лежит (мы же помним по call что при записи в стек ESP уменьшается), и подставит вместо num внутри скобок [] правильный адрес. Ничего сложного. И работает.
Разумеется такое можно проделывать не только в самом начале процедуры, но и в любом её месте. Т.е. вовсе не обязательно читать аргументы в регистры и сохнуть над ними, можно оставить где лежат в памяти и каждый раз обращаться туда. Медленнее, зато экономит регистры. И как будет выгоднее в конкретном коде ещё большой вопрос! С современными кэшами и шириной шин в процессорах лишний свободный регистр-два могут заметно ускорить другие вычисления, с лихвой перекрывающее тормоза чтения памяти (реально кэша). Так что этот, описанный во всех книгах по оптимизации, метод (все совать в регистры) весьма спорен в современности.

А теперь важные тонкости про аргументы.
В момент попадания управления в процедуру по адресу ESP лежит адрес возврата, он нам пока не нужен, а выше лежат аргументы, всегда по 4 байта каждый. Значит первый аргумент мы можем прочитать по адресу [ESP+4] (сразу выше адреса возврата), второй аргумент по адресу [ESP+8] и так далее. Можем. Если не пользовались подарком FASM proc и endp! (Про исключение снова промолчу, не уверен что оно документировано и не изменится в следующей версии FASM.) А если пользовались, как всех призываю, то в начале функции (и в конце, но там не так страшно) появится дополнительный код, которого вы не писали, и который изменит ESP! Давайте уже посмотрим на реальный код, сколько можно болтать:
Используется синтаксис ASM
;Было в тексте (чуть сократил)
proc isprime stdcall, num
        mov     ECX,[num]
;Стало в реальном коде для процессора:
        push    EBP
        mov     EBP,ESP
        mov     ECX,[EBP+8]
Чуете? Зачем-то сохранили регистр EBP и записали в него новое значение ESP. А потом ещё и лазить к аргументам стали не по ESP, а по EBP! Чушь какая-то. Что делать и кто виноват?
Ничего не делать, а виноват конечно FASM. Потому что вы его попросили! Командой proc. Писали? Писали. Получите.
Реально конечно это не баг, а фича, и не в шутку, а правда. Что бы дальше в функции мы не делали с ESP (а команды push и обратную к ней pop нам никто не запрещал) и как бы он не дрыгался в процессе наших запутанейших вычислений, регистр EBP так и будет сохранять адрес самого себя в стеке. И можно адресоваться в аргументам относительно него, не заморачиваясь насколько в каждом конкретном месте изменился ESP командами push без парной ей pop. А смещение +8 вместо +4 потому что кроме аргументов и адреса возврата мы туда положили ещё и регистр EBP и только потом уже уменьшенное значение ESP запомнили в EBP.
Это одна причина. Вторая в том что функциям часто нужны локальные параметры (уникальные для каждого вызова функции, не для куска кода, а даже когда код, а вызвали много раз (например сама себя рекурсивно)). И кроме стека их хранить нигде неудобно. А в стеке - удобно, каждый вызов двигает ESP ниже и ниже и ничего выше не портит. Потому делаем так: после показаных двух команд уменьшаем ESP на величину необходимой нам памяти в стеке. И вуаля: все следующие записи в стек пойдут ещё ниже и ничего нам в зарезервированной памяти (от [ESP] и до [EBP]) не попортят, а адресоваться к этой памяти можно удобно через [EBP-xxx], знаковые константы никто не запрещал.

Поэтому важный момент: регистр EBP обычно уже использован FASM-ом под служебные цели и недоступен для вычислений! Остаётся лишь 6 регистров, вот такая беда. Зато удобно пользоваться процедурами/функциями.

Остался неосвещённым вопрос о выходе из процедур, ведь туда уже EBP положили, ESP поменяли, как теперь до адреса возврата добраться и что делать с тучей аргументов выше него. Ну, рвать волосы рано, за нас подумали и Intel и авторы FASM - команда endp меняет нашу любовно написанную команду ret на что-то левое:
Используется синтаксис ASM
;Было в тексте
        ret
endp
;Стало в реальном коде для процессора:
        mov     ESP,EBP
        pop     EBP
        ret     4
Или правое? Mov возвращает ESP чтобы он снова указывал на сохранённую копию EBP в стеке и адрес возврата выше и аргументы (сколько бы ни было зарезервировано памяти для локальных переменных), команда pop вынимает из стека значение регистра EBP на момент запуска процедуры, команда ret с числом возвращает управление откуда вызвали и дополнительно увеличивает ESP на указанное число - что "удаляет" из стека и единственный аргумент функции isprime (мы сами про него сказали в команде proc). Выходит что после возврата на команду после invoke/stdcall всё вернётся в исходное состояние, и указатель стека ESP, и адрес выполнения, и регистр EBP. Значит не левое, а очень даже правое и нужное. И ничего самим помнить и писать не надо, endp и привет.

И вот тут появляется обещанная засада: это прекрасно срабатывает если ret стоит в самом конце (про исключения молчу, пальцы болят ;-)) функции и всего один на всю функцию, где его и видит FASM и правильно меняет на вон те команды. А все другие ret он не видит (недостаточно умный) и не меняет! И если туда попадёт управление, то будет беда. Та или другая, но точно будет. Потому при использовании proc/endp надо чтобы ret был всегда один на всю функцию/процедуру и в конце, прямо перед endp. Если нужен где-то ещё - ставьте там команду jmp на вот этот последний ret.

Зачем я в объявлении isprime командой proc указал ещё и uses? Ну, мне в isprime понадобятся какие-то регистры для работы, в показанном коде нужны оказались EBX ECX EDX ESI - но ведь они же могли использоваться и там откуда вызывали isprime! И что делать ... Неужели помнить какая функция какие регистры использует и не хранить в них ничего на момент вызова каждой функции?! Ну, можно и так. Только регистры быстро закончатся, да и голова опухнет. Сделаем проще (но не значит лучше! тут мнение у каждого своё): при входе в функцию сохраним куда-нибудь все регистры которая она использует, а при выходе восстановим оттуда. Решение? Решение. Можно сохранять в память, но если эту же функцию вызовут дважды или рекурсивно (сама себя) будет беда. Выход нам уже известен - стек наше всё. Можно всё делать самому руками, уменьшать ESP, писать туда ([ESP+xxx]) регистры, потом при выходе их читать (кстати это будет быстрее стандартного способа! под x64 примерно к этому и пришли), можно пользоваться командами push для сохранения и pop для восстановления (в обратном порядке! очень легко забыть), а можно перепоручить эту муть FASM-у указав какие регистры нам нужны в процедуре и пусть сам парится как их сохранить и восстановить. Словом uses с перечислением регистров через пробел. Что при этом получается покажу чуть ниже.
А пока ещё момент. Программистам быстро надоело писать каждый раз кучи push и pop подряд (FASM тогда ещё не было) и Intel пошла им навстречу сделав команды pushad и popad - запихать сразу все регистры в стек или достать их оттуда. Я использовал их выше в "заклинании" для краткости. Хорошие команды. Три НО: медленные (8-9 тактов на 8 регистров); отсутствуют в x64; popad восстановит все регистры, и EAX тоже, а там ведь должен лежать результат вычислений функции ... Все три можно обойти, но тогда и выигрыш от всего одной команды станет не столь заметным.

Ну и наконец давайте же посмотрим во что превращается код процедуры при использовании и аргументов, и рабочих регистров, и локальных переменных:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
;Было в тексте:
proc f1 stdcall uses EAX ECX, num
local x:DWORD, s[18]:BYTE
        mov     ECX,[num]
        movzx   EAX,[s+0]
        add     EAX,ECX
        add     EAX,[x]
        ret
endp
;Станет в коде и увидит процессор:
        push    EBP             ;Сохраним старое значение EBP
        mov     EBP,ESP         ;Теперь он будет указывать и на локальную память, и на адрес возврата, и на аргументы
        sub     ESP,24          ;Зарезервировали 24 байта локальной памяти, 20 на s[] (кратно 4) и 4 на x
        push    EAX             ;Сохранение рабочих регистров
        push    ECX

        mov     ECX,[EBP+8]     ;Это как ни странно первый аргумент, он именно там лежит
        movzx   EAX,byte [EBP-20];Массив s[] выровняли на 4 байта (до 20) и положили в тек первым (выше)
        add     EAX,ECX
        add     EAX,[EBP-24]    ;Переменная x оказалась ниже s[]

        pop     ECX             ;Восстановление рабочих регистров
        pop     EAX
        mov     ESP,EBP         ;Сняли резерв локальной памяти если был
        pop     EBP             ;Восстановили EBP как был на входе
        ret     4               ;Возврат с удалением одного аргумента (они все ровно по 4 байта)
Было 5 явных команды, стало 14, 9 команд добавил FASM (по нашему же указанию в виде proc, local, endp).
Если посмотреть на реальный скомпилированный код, то там вместо последних mov и pop будет непонятная leave. Делает ровно то же самое, а код всего один байт. Ну и что что 6 тактов вместо 1 (да, две команды могут выполниться за 2 такт), зато какая экономия байтов! Это один из пережитков эпохи 8086/80286/80386, когда памяти было мало и экономили длину команд как могли. Вопросы к авторам FASM.

PS. Всё, 20К текста, больше низя.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 04:22 
Заслуженный участник


20/08/14
11210
Россия, Москва
Так, выше невнятно об отличиях invoke и stdcall: обе вызывают функции, но вторая только из нашего же исполняемого файла (.exe), а первая строго из других исполняемых файлов (.dll, насчёт .exe не вполне уверен). Функции WinAPI и много других поставляются обычно в .dll файлах, это удобно. Как дойдёт дело покажу как функции из cmdline_32.inc и isprime3_32.inc переделать в отдельные .dll и использовать аналогично функциям WinAPI (уже через invoke). Можно будет не трогать главный .exe при незначительных изменениях в функциях в .dll, т.е. перекомпилять только их. В принципе можно наверное этим и заняться, ради удобства.
Если в proc нет рабочих регистров (uses) и нет параметров и локальных переменных, то никакого лишнего кода в функцию не добавляется и её можно вызывать как stdcall, так и обычным call, разницы никакой (и тогда регистр EBP остаётся доступным для вычислений). Т.е. процедуры, написанные когда-то давно без механизма proc/endp, вполне можно для единообразия вызывать и через stdcall вместо call.

Почитал вики про соглашение о вызовах для x32 (почти не пишу под неё код на асме), оказывается сохранять рабочие регистры в функциях надо только EBX, ESI, EDI, EBP, а про EAX, ECX, EDX это морока вызывающей программы, их процедуры могут использовать как угодно (и их в uses в proc указывать не нужно).
Ну и кроме stdcall метода передачи аргументов есть ещё куча, но так как stdcall нужен для использования WinAPI, то для единообразия проще его же использовать везде. По крайней мере пока не станем достаточно гуру чтобы понимать где уместно иное (я то ли ещё не стал, то ли уже перестал таким быть - везде юзаю stdcall и не парюсь).

Кстати, про команду leave. Она экономит всего 2 байта! Команда mov занимает два байта, а pop один. Жалкие два байта на всю огромную функцию! Уж лучше быстрее чем лишних два байта. Здесь я авторов FASM не понимаю.


В принципе про функции и процедуры всё необходимое сказано.
Показывать ли несколько забавных трюков - даже и не знаю, они почти все несовместимы с механизмом proc/endp, а лучше пользоваться им. Или не рекомендуются к использованию (с чем я понятно не совсем согласен).
К x64 никто интереса не проявил, там вызов процедур замороченнее. Хотя если пользоваться proc/endp/local, то разницы практически и нет.

Должны были появиться вопросы ... Или все остальные всё это и так знали? Ну ОК.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 10:14 
Аватара пользователя


29/04/13
7255
Богородский
Чтоб не думали что я сдался. Пока не получается напечатать то, что хотел.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 11:47 
Аватара пользователя


29/04/13
7255
Богородский
Dmitriy40 в сообщении #1626444 писал(а):
Ещё, процедура IntPrimes это фактически решето Эратосфена для чисел до 65536 с битовым массивом только для нечётных чисел. Он и потом используется, так втрое меньше памяти надо чем если хранить все 6541 простое в прямом виде

Вот такое вычёркивание в такой битовой карте?

Код:
     111  11222223  33334444  45555566  66677777
13579135  79135791  35791357  91357913  57913579
01111111  11111111  11111111  11111111  11111111

     111  11222223  33334444  45555566  66677777
13579135  79135791  35791357  91357913  57913579
01110110  11011011  01101101  10110110  11011011

     111  11222223  33334444  45555566  66677777
13579135  79135791  35791357  91357913  57913579
01110110  11010011  00101101  10100110  01011011

     111  11222223  33334444  45555566  66677777
13579135  79135791  35791357  91357913  57913579
01110110  11010011  00101101  00100110  01011001

Как бы такое напечатать для проверки?

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 15:13 
Заслуженный участник


20/08/14
11210
Россия, Москва
Ну и задачка, больше часа потратил написать показ битового массива, только где-то с 15-го запуска стало показывать как задумывал.
Вывод отладочной инфы оформим как процедуру, ничего не портящую (считаю это хорошим стилем), чтобы можно было вставлять в разные точки и проверять "а что там?". Меняем файл isprime3_32.inc, в нём в процедуре InitPrimes прямо по метке .sieve и перед командой inc добавляем вызов отладочной процедуры stdcall DebugDisp, EBX - в EBX сидит индекс уже вычеркнутого числа, его тоже покажем для справки, не самому же считать. ;-)
Сразу за процедурой InitPrimes (после её команды endp) добавляем кусок кода с новой процедурой:
код: [ скачать ] [ спрятать ]
Используется синтаксис ASM
proc DebugDisp, num
                pushad                          ;Сохраним всё, на всякий случай
                mov     ESI,[num]
                cmp     ESI,16/2
                jnc     .exit                   ;Числа больше 16 показывать не будем
                mov     EDI,DebugText2          ;Здесь символы битовой карты
                mov     EBX,0                   ;С какого смещения начинаем показ
.cycle:         mov     EAX,'0'
                bt      [primes],EBX            ;Проверяем бит в массиве
                adc     EAX,0                   ;Добавляем к символу '0' бит переноса (и он может превратиться в симво '1')
                mov     [EDI],AL                ;Сохраним правильный символ в строку
                inc     EDI
                inc     EBX
                test    EBX,0x7                 ;Проверяем на пересечение границ байтов чтобы учесть пробел
                jne     .n8
                inc     EDI                     ;Пробел пропускаем, он к битовому массиву отношения не имеет
.n8:            cmp     EBX,100/2               ;Показываем только первые 100 нечётных чисел
                jc      .cycle
                inc     EDI                     ;Пропуск символа ':'
                mov     EDX,0
                mov     EAX,[num]               ;После обработки какого числа показан массив
                lea     EAX,[EAX*2+1]           ;Превратим индекс в число
                mov     ECX,10
                div     ECX                     ;В EAX старшая цифра, а EDX младшая
                mov     AH,DL                   ;Объединим в одни регистр AX, причём цифры в обратном порядке, ведь в строке первым идёт старшая
                add     EAX,'00'                ;Превратим в символы
                mov     [EDI],AX                ;И запишем в строку
                invoke  WriteFile, dword [hErr], DebugText1, DebugTextLen, temp, 0      ;Местоположение и длина строки всегда фиксированы
.exit:          popad
                ret
endp
А в самом конце файла, между строками align и primes добавляем выводимую строку текста:
Используется синтаксис ASM
DebugText1:     db      '     111 11222223 33334444 45555566 66677777 88888999 99',13,10
                db      '13579135 79135791 35791357 91357913 57913579 13579135 79',13,10
DebugText2:     db      '-------- -------- -------- -------- -------- -------- --:--',13,10,13,10
DebugTextLen = $-DebugText1
Эта строка - данные, потому сидит в секции .data. А так как мы точно знаем что именно будем выводить (а не вычисляем это по ходу дела), то и команду вместо rb ставим db (выделение памяти в виде байтов под аргументы) и пишем текст в виде строковой константы.
Так как отладочная процедура пишет в эту строку (на места с символами '-'), то эту строку нельзя положить в секцию кода, туда запись запрещена, можно только читать и выполнять, потому место ей в секции .data.
Левый символ '-' использован специально - если где-то ошибка будет видно что какой-то байт не обновился процедурой. Советую фишку запомнить.
Так как хотим вывести сразу всю строку, то и указываем её по метке DebugText1 сразу длинной, и без всяких посторонних нулей, они для WriteFile не нужны (и она их честно выведет в файл или на экран!), ей подавай просто длину в байтах. Длину в байтах вычисляем как разницу текущего адреса (специальная переменная $ в FASM) после всей строки и адреса начала строки - пусть парится FASM с подсчётами, мне лень.
Чтобы не заниматься мазохизмом и не вычислять по какому смещению в длинной строке начинаются символы '-', которые и будем обновлять, ставим на их начало ещё одну метку.
Обратите внимание что везде используются полные 32-разрядные регистры и только в момент записи в память пишется ровно то количество байтов, что нужно (два, т.е. две десятичные цифры для числа и один байт для отображения битового массива).
Заметьте, банально показать информацию заметно длиннее вычисления.
С комментариями проблема - не везде адекватны, кое-где могли остаться от трёх предыдущих версий процедуры, не заметил. Будет тестом на понимание. :mrgreen: Как обычно и поступают истинные программисты (sic!), любую багу объявляем фичой и успокаиваемся. ;-)

Запускаем:
код: [ скачать ] [ спрятать ]
Используется синтаксис Text
C:\>prog3.exe 0
     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01111111 11111111 11111111 11111111 11111111 11111111 11:01

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11011011 01101101 10110110 11011011 01101101 10:03

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11010011 00101101 10100110 01011011 01001100 10:05

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11010011 00101101 00100110 01011001 01001000 10:07

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11010011 00101101 00100110 01011001 01001000 10:09

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11010011 00101101 00100110 01011001 01001000 10:11

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11010011 00101101 00100110 01011001 01001000 10:13

     111 11222223 33334444 45555566 66677777 88888999 99
13579135 79135791 35791357 91357913 57913579 13579135 79
01110110 11010011 00101101 00100110 01011001 01001000 10:15

time: 0.001s
Что и требовалось.
Убирать составные числа не стал специально, можно поглядеть что ничего не меняется.
Закомментировав по метке DebugText1 первые два db (строки с номерами чисел), можно оставить только сам битовый массив - проще смотреть что меняется.

-- 20.01.2024, 15:24 --

После изучения как вычёркиваются биты можно заметить что первым вычёркиваемым числом реально является квадрат простого, до него все кратные этому простому вычеркнуты меньшими простыми. Да, так и есть (тривиально доказывается математически). Но я поленился (а это как известно двигатель прогресса!) переводить индекс в число (ага, одной командой), вычислять квадрат (тоже кажется хватит одной команды), приводить его обратно к индексу (пусть и одной командой) и вычёркиваю начиная со следующего нечётного кратного после самого простого. Немного дольше, но для такого маленького решета значения не имеет. "Не стоит оптимизировать то что и так работает достаточно быстро!" Впрочем для тренировки можете сделать, не так уж и сложно, все нужные команды уже есть где-то по тексту.

-- 20.01.2024, 15:26 --

Yadryara
А что там с задачкой ограничения перебора не до конца таблицы? Это ведь в isprime надо делать, не в решете.
Т.е. как работает решето конечно интересно, сам поглядел, но это немного другое.

И дополнительный вопрос: все ли команды и их смысл (и назначение - зачем они, какую роль выполняют в задаче показа инфы) понятны в процедуре DebugDisp? Должно быть да. Если нет - давайте разбираться, в ней никакой математики или забавных алгоритмов нет, почти всё по рабоче-крестьянски (собственно так специально).

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 16:17 
Аватара пользователя


29/04/13
7255
Богородский
Благодарю. У Вас здорово получается.

Dmitriy40 в сообщении #1626589 писал(а):
А что там с задачкой ограничения перебора не до конца таблицы?

Вслепую с помощью MUL делать не стал. Решил сначала научиться печатать все необходимые числа. Но не научился. Как печатать проверяемое число (num?), например. Как только обрету зрение, буду гораздо лучше понимать какая команда что делает.

Dmitriy40 в сообщении #1626589 писал(а):
И дополнительный вопрос: все ли команды и их смысл (и назначение - зачем они, какую роль выполняют в задаче показа инфы) понятны в процедуре DebugDisp?

Не все, обязательно ещё подумаю.

Dmitriy40 в сообщении #1626589 писал(а):
Немного дольше, но для такого маленького решета значения не имеет. "Не стоит оптимизировать то что и так работает достаточно быстро!"

Это хорошо понятно.

Мне бы слепоту побороть, это важнее, вроде.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 16:41 
Заслуженный участник


20/08/14
11210
Россия, Москва
Yadryara в сообщении #1626595 писал(а):
Решил сначала научиться печатать все необходимые числа. Но не научился. Как печатать проверяемое число (num?), например. Как только обрету зрение, буду гораздо лучше понимать какая команда что делает.
Э, я же приводил уже "магию" для печати любого числа:
Dmitriy40 в сообщении #1626503 писал(а):
Yadryara в сообщении #1626502 писал(а):
Я даже не понимаю толком как те или иные числа печатать хотя бы на экран.
Это несложно: в "заклинании" ниже указываем вместо ECX где у нас лежит число и вставляем это "заклинание" где нам надо и радуемся жизни:
Используется синтаксис ASM
                pushad  ;Нужно только если код вставляем куда-то в середину своей программы, где нельзя портить регистры
                invoke  wsprintf, buf, .fmt99, ECX
                invoke  WriteFile, dword [hOut], buf, EAX, temp, 0
                popad   ;Нужно только если код вставляем куда-то в середину своей программы, где нельзя портить регистры
...
.fmt99:         db      '%u, ',0        ;Это кладём вместе с остальными строками форматов после процедуры куда вставляем
Со строкой должно быть всё понятно, обычный формат для printf из C или PARI/GP. Лишь не забыть что любая строка должна завершаться символом 0x00. А перевод строки я предпочитаю указывать явно как 13,10 (0x0D,0x0A). Ну что строки в одинарных кавычках вместо двойных и так понятно по примерам.
Разумеется если надо несколько разных строк формата, то каждой даём свою метку и их и указываем в wsprintf.
Если надо не из регистра ECX, а скажем из памяти по адресу/указателю num, то меняем ECX на dword [num].
Ну как бы да, вставлять в нужное место 4 строки не слишком удобно, ну так теперь можно и в виде процедуры оформить:
Используется синтаксис ASM
;Примеры использования:
        stdcall Disp, dword [num]       ;num это указатель где в памяти лежит число dword
        stdcall Disp, EDX               ;Из EDX
        stdcall Disp, 12399             ;Просто конкретное число

;Сама процедура:
proc Disp, nn
        pushad  ;Всё сохраним
        invoke  wsprintf, buf, .fmt, dword [nn]         ;Пользуемся общим буфером под строку buf
        invoke  WriteFile, dword [hErr], buf, EAX, temp, 0
        popad
        ret
endp
.fmt:   db      '%u',13,10,0

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 17:09 
Аватара пользователя


29/04/13
7255
Богородский
Dmitriy40 в сообщении #1626597 писал(а):
Э, я же приводил уже "магию" для печати любого числа:

Ну вот не сработала у меня эта магия. Попробую через процедуру. Её в тот же файл isprime3_32.inc писать? А сразу после неё строка с .fmt: ?

-- 20.01.2024, 17:30 --

Опять не работает. Из какого места её нужно вызывать?

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 17:35 
Заслуженный участник


20/08/14
11210
Россия, Москва
А давайте тогда уж расскажу про отладчик, чтобы можно было прямо по шагам по каждой команде смотреть что происходит и в регистрах и в памяти. Это третий необходимый инструмент программиста после редактора и компилятора. Я пользуюсь этим: https://x64dbg.com - он бесплатный и поддерживает и x32 и x64 и AVX2 (что для меня ценно). Но непростой. Опишу только самые необходимые азы.
Скачиваем, устанавливаем, запускаем. Должно открыться пустое окно. В Options/Параметры последним пунктом можно поменять язык, я поставил русский, огрехов не помню, вполне хорошо переведено. В первом пункте Параметры-Параметры закладка События ставим галку на Точка входа.
Левая иконка или пункт в меня открыть файл - ищем где наш prog.exe и открываем. Должна получиться красивая картинка примерно как первая на заглавной странице родного сайта. Слева код, справа регистры, внизу слева память, справа содержимое стека. Размеры и размещение окон - мышкой. Показать/скрыть окна через меню Вид. При этом первая строка в окне кода с call GetTickCount должна подсвечиваться серым фоном, а левее синяя стрелка с название EIP (Если там и EDX - плевать, не нужно). Это следующая выполняемая команда.
Запустилась ещё одна программа - как раз наша prog.exe, из под отладчика, мы ж её и исследуем, а в её окне показано что она выводит.
В меню Файл-Изменить аргументы командной строки добавляем к имеющейся строке с именем прогмамы с путём ещё и два числа 10 113 - это будут параметры в нашу программу.
В меню Отладка две главные команды F7 и F8 - выполнить одну команду даже если она call, и выполнить одну команду, но если call, то дождаться возврата из процедуры, т.е. в неё внутрь не заходить. Ну зачем вам смотреть как GetTickCount по винде гуляет? Незачем. Нажимаем F8. Курсор (подсветка и синяя стрелка) смещается на вторую строку. При этом справа регистры подсвечиваются красным (если все чёрные - скажите, поищу где это в настройках выключено) - эти регистры изменились при выполнении команды. А мы выполнили всю процедуру GetTickCount. И после неё важен лишь EAX, там время. Остальные (EDX, EIP) изменились "паразитно" (кроме EIP, который указывает на следующую выполняемую команду).
Теперь подсвечена команда mov. Её можно выполнить как F8, так и F7, это не call. Предлагаю всегда пользоваться только F8. За одним единственным исключением.
Итак, пощелкайте F8, каждый раз смотря какие регистры как меняются.
Двойной щелчок мыши на регистре покажет его значение в разных форматах. Там же можно его и поменять - не стоит!
Правой мышой на пустом месте окна регистров можно выбрать какие-то опции отображения регистров - попробуйте разные.
Да, слева в окне кода будут ещё пунктирные линии слева от кода - это как/куда передаётся управление командами переходов. Удобно видеть что на какую-то команду можно попасть из нескольких мест.
При отладке удобно держать перед глазами открытый асм исходник - отладчик не знает ни о ваших именах функций, ни именах меток, ни других именах, ни тем более комментариев не видит.
Предлагаю дощелкать F8 (не заходя внутрь процедур) до первого je, при этом линия слева будет по прежнему серой - переход je выполнен не будет - ведь мы ввели первым число 10, которое ну явно не 0.
Щелкаем дальше F8 до команды jle немного ниже, линия слева стала красной - переход jle будет выполнен. Нажимаем F8 и да, вернулись чуть выше.
Повторяем F8 пока не попадём чуть ниже jle. надеюсь Вы последовали совету и указали первым число 10, а не миллион и смогли дощёлкать F8 нужное число раз.
Теперь полезная фишка: вторая слева иконка в виде стрелки по кругу (или в меню Отладка - Перезапустить CTRL-F2) - как будто закрыли и открыли заново, удобно, делаем. Курсор вернулся на самое начало программы. Фишка: щёлкаем по команде ниже jle до которой мы доходили в прошлый раз и нажимаем F4 (Отладка - Выполнить до выделенного) - курсор перескочил на выделенную команду, первые 4 регистра красные (вся суммарная куча команд выше их поменяла), в окне выполняемой программы появилось море текста про вычёркивание битов. Смотрим строк на 10 ниже, ищем call WriteFile (примерно, не уверен как у вас показывается), щёлкаем после неё, снова F4 - в окне работающей программы появилась строка о количестве простых до 10 - и неправильное! Оп-па. Где-то у меня ошибка появилась при дополнении вывода карты битового массива. Пока наплюём.
Снова сделаем Ctrl-F2 (вторая слева иконка, перезагрузить заново).
Теперь дощёлкиваем F8 до команды call 4012AB (отображение чисел в HEX включается где-то в настройках), это stdcall InitPrimes в исходнике. Это вторая команда call без комментариев отладчика (имени функции из WinAPI). Она то нам и нужна.
Чтобы зайти в неё и посмотреть как оно там внутри шебуршится нажимаем F7 (Отладка - Шаг с заходом) - и курсор перескакивает на команду push EBP. Это уже начало нашей процедуры.
Снова щёлкаем F8 и изучаем что происходит.

Теперь как память смотреть, ту что слева внизу. Второй командой в нашей процедуре стоит mov EAX,.... Выполняем её, регистр EAX справа красный - его изменили. Щёлкаем на нём правой мышой и выбираем пункт Перейти к дампу. Чиселки в левом нижнем окне изменились, на нули. Щёлкаем в нём правой мышой и выбираем пункт Шестнадцатиричное - ASCII. Формат показа изменится на обычный hex, слева 16 байтов, справа они же в виде символом (или точек если символ не отображаемый). Сейчас вся таблица нулевая. В битах отобразить нельзя, жаль.
Курсор в окне кода на второй команде mov (третья команда в процедуре). Нажимаем F8 и левые 4 байта в окне дампа меняются на FE FF FF FF - да, именно это туда и записали.
Ещё несколько раз F8 смотрим как заполняется массив primes (а это он, смотрим в свой исходник что мы в EAX писали). Как надоест (а там больше 16380 команд надо прощёлкать) щёлкаем на команду после jb (цикл заполнения массива) и жмём F4 - хоп-па, весь массив внизу обновился. Ну, так и должно.
В общем и так далее.

Если вдруг запутались и потерялись или попали непойми куда (не смущаемся, я тоже совсем нередко), то вторая слева иконка (или Ctrl-F2), перезагрузить сначала и всё повторяем. Активно пользуемся F4 чтобы пропустить все неинтересные циклы и процедуры. F7 нажимаем только если совершенно уверены что нам надо внутрь подсвеченного call, иначе всегда F8 (или F4).

Если нашли ошибку или другая мысль пришла и надо программу перекомпилить, то в отладчике ей надо закрыть, третья иконка слева или Отладка-Закрыть. Иначе FASM не сможет создать новый .exe. Кстати и повторно открывать файл можно сразу второй иконкой (CTRL-F2, Перезагрузить), не обязательно снова выбирать файл через первую иконку.

-- 20.01.2024, 18:03 --

Yadryara в сообщении #1626599 писал(а):
Ну вот не сработала у меня эта магия.
Блин, вот почему и плохо писать код из головы, не проверяя, даже такой очевидный. И у меня не работает (WriteFile стек портит, хотя об ошибках не сообщает, и в итоге прога вылетает по ошибке). Разбираюсь. Надо было сразу сказать.

 Профиль  
                  
 
 Re: Первые и последующие шаги в ассемблере x86/x64
Сообщение20.01.2024, 18:44 
Заслуженный участник


20/08/14
11210
Россия, Москва
Охренеть. Это опять моя невнимательность, оказывается функция wsprintf хоть и сидит в WinAPI, но использует другое соглашение о вызовах, как в языке С (впрочем в описании к ней это сказано, пропустил) - стек за собой чистит тот кто вызвал! Так что проблема не в WriteFile, она честно убирает свои аргументы из стека, а в wsprintf, которая свои не убирает. Выходит и у меня во всех программах тот же глюк сидит необнаруженным ... :facepalm: Правда под x32 я практически не пишу, а под x64 этот глюк не проявляется.

К счастью авторы FASM об этом подумали: надо всего лишь invoke wsprintf везде (во всех файлах!) заменить на cinvoke wsprintf - чтобы использовался С-ый стиль вызова. Только в вызовах wsprintf.

Доработанную процедуру удобно добавить в главный файл, в конец, сразу перед двумя include:
Используется синтаксис ASM
proc Disp stdcall, nn
        pushad  ;Всё сохраним
        pushf   ;И все флаги тоже
        cinvoke wsprintf, buf, .fmt, dword [nn]         ;Пользуемся общим буфером под строку buf
        invoke  WriteFile, dword [hErr], buf, EAX, temp, 0
        popf
        popad
        ret
endp
.fmt:   db      '%u',13,10,0
Использовать (вызывать) можно где угодно, буквально в любой точке кода, командой stdcall Disp, EDX (или что именно показать, примеры были выше).
Теперь у меня работает, везде. Скажите если снова не получится.

-- 20.01.2024, 18:57 --

Dmitriy40 в сообщении #1626603 писал(а):
в окне работающей программы появилась строка о количестве простых до 10 - и неправильное! Оп-па. Где-то у меня ошибка появилась при дополнении вывода карты битового массива. Пока наплюём.
Ложная тревога, в показанном выше тексте ошибки нет, это я её у себя добавил когда стал править isprime3_32.inc для соответствия соглашению о вызовах. Показанный код функций лучше требуемого (так как не портит даже то что можно). Но править тогда надо и почти весь главный исходник (вон он нарушает соглашение о вызовах). Забил, пока оставим как есть.

Кстати в отладчике очень удобно смотреть что именно из регистров портит (меняет) любая процедура - доходим до неё (чтобы курсор указывал на call), нажимаем F8 - и смотрим на красные регистры, они изменились.

 Профиль  
                  
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ Сообщений: 133 ]  На страницу Пред.  1 ... 3, 4, 5, 6, 7, 8, 9  След.

Модераторы: Karan, Toucan, PAV, maxal, Супермодераторы



Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group