В настоящее время я пытаюсь изучить MASM x64, и пока, кажется, я довольно хорошо разбираюсь в вещах. Все шло хорошо, пока я не попытался вызвать CreateFileW для чтения содержимого файла .txt. Проблемный код выглядит следующим образом:
; Open the file for GENERIC_READ
CALL ClearRegisters
LEA RCX, TextTestfilePath
MOV RDX, 80000000h ; GENERIC_READ
MOV R8, 00000001h ; FILE_SHARE_READ
MOV R9, 0h ; NULL
SUB RSP, 40h
PUSH 0h ; NULL
PUSH 80h ; FILE_ATTRIBUTE_NORMAL
PUSH 3 ; OPEN_EXISTING
CALL CreateFileW
ADD RSP, 40h
CMP EAX, -1
JNE p_skip_invalid_create_file
CALL InternalError
p_skip_invalid_create_file::
Это часть полной программы, которую можно найти здесь.
Когда я запускаю программу, я ввожу в программу свой файл («test.txt») (test.txt находится в исходных файлах, которые также можно найти на GitHub). TextTestfilePath — это сохраненное значение этого вывода ReadConsoleW (с усеченным CRLF в конце). В памяти он читается как 0074 0065 0073 0074 002e 0074 0078 0074 0000
или «.t.e.s.t…e.x.e..», что, насколько я понимаю, является действительным Unicode.
При выполнении кода CreateFileW возвращает -1 или INVALID_HANDLE_VALUE, а после вызова GetLastError я получаю 0x57 или ERROR_INVALID_PARAMETER. Я попытался вызвать SetLastError, чтобы установить его на ноль перед вызовом и получить тот же ответ.
После небольшого разговора с GPT-4 я все еще не могу найти источник проблемы. Я проверил следующее:
Я все еще изучаю MASM x64 с ограниченной информацией о нем, но у меня есть общее понимание того, как все это работает, и я прочитал несколько книг об этом и использовал часть Win32 Console API до эта точка.
Но каждый раз, когда я получаю эту ошибку параметра, я теряюсь. Это настолько расплывчато, что я не знаю, где на самом деле проверить, и все, что я проверяю, никогда не кажется проблемой. Поэтому, если у кого-то есть какие-либо идеи о том, что мне нужно проверить (или, черт возьми, если вы видите проблему) (или, черт возьми, если у вас есть какие-либо советы для меня, которые я еще не понял), пожалуйста, дайте мне знать ! :)
Прежде чем комментировать, что я делаю что-то не так, пожалуйста, помогите не только мне, но и сообществу найти хорошо документированные источники для изучения MASM x64, которые могут хорошо объяснить эту концепцию! Простое высказывание «Вы делаете что-то не так, и вам нужно это исправить» не помогает решить эту проблему и не способствует обсуждению, которое поощряет обучение и образование, чего можно было бы ожидать от такого сайта, как StackOverflow. Ссылки на сторонние источники, в дополнение к очевидным документам Microsoft, невероятно полезны для общего обзора того, что ожидается, вместо того, чтобы предполагать, что определенные вещи известны, когда они могут быть неизвестны.
@RbMm Наверное, я должен был отметить, я также пробовал SUB RSP, 20h; SUB RSP, 50h и различные другие позиции стека безрезультатно. Я не нашел хороших ресурсов, чтобы научить меня соглашениям по программированию MASM x64, поэтому я собирал по кусочкам то, что мог, где мог. Я был незнаком с синтаксисом MOV [RSP+20h], значение. Хотя в документации Windows мало что говорится о точных позициях значений в стеке, по крайней мере, из того, что мне удалось найти. Я уверен, что где-то что-то пропустил. Если у вас есть какие-либо ресурсы, которые могли бы указать мне правильное направление, это было бы невероятно полезно!
learn.microsoft.com/en-us/cpp/build/stack-usage?view=msvc-170
CMP EAX, -1
также формально не правильно, несмотря на то, что всегда будет работать нормально. должно быть CMP RAX, -1
. вместо этого CALL CreateFileW
лучший пользователь CALL __imp_CreateFileW
@RbMm Я ценю понимание. Я не был знаком с нотацией __imp_ для доступа, поэтому я все определял вручную. Я использую PROTO в коде. Я много раз читал эту страницу стека, но, должно быть, у меня не выходит из головы, потому что я понятия не имею, как то, что там упоминается, переводится в MOV [RSP+20h], VAL. Лучшее из того, что он упоминает, - это «поместить аргументы в стек», повторенное три раза, без упоминания о том, что это за смещение или как далеко оно должно идти. Из того, что я узнал через ChatGPT, это 20-часовая тень для первых 4, +8 для каждого аргумента стека, выровненная по ближайшим 16
СИЛЬНОЕ ПРЕДЛОЖЕНИЕ: 1) Напишите программу на C, которая вызывает Win32 API "CreateFile()". 2) Разобрать: cl /FAs mytest.c
Я ценю понимание @paulsm4! Я нашел серию, которая делает то же самое с их примером функции добавления, вы бы сказали, что это похоже на то, что получается из этого? medium.com/@sruthk/…
Я понял это. Я действительно неправильно нажимал на стек. Но у меня было фундаментальное непонимание того, как работает стек, что документация Microsoft проделала ужасную работу по объяснению.
Как указал @RbMm в комментариях, ожидается, что аргументы будут на RSP+20h, RSP+28h и RSP+30h соответственно. Кроме того, в стеке должно быть теневое пространство для вызова функции. Я делал ряд ошибок, из-за которых это не сработало.
Давайте объясним, как я делал код ранее:
LEA RCX, TextTestfilePath
MOV RDX, 80000000h ; GENERIC_READ
MOV R8, 00000001h ; FILE_SHARE_READ
MOV R9, 0h ; NULL
SUB RSP, 20h
PUSH 00h
PUSH 80h
PUSH 3
CALL CreateFileW
ADD RSP, 20h
Я модифицировал указатель стека, чтобы вытолкнуть теневое пространство. Это правильно, и 20h является правильным значением для этого, потому что это 32 байта теневого пространства, которое переводится в 20h в шестнадцатеричном формате. Это сохранит все 16-битное выравнивание.
Я помещал аргументы в стек. Проблема в том, что я делал это неправильно (или наоборот). RSP или указатель стека ссылается на вершину стека. Когда я PUSH помещал значения в стек, он помещал значения выше в стек. В довершение всего он изменит указатель стека так, чтобы он больше не был выровнен по 16 битам. Ожидается, что указатель стека будет иметь значение 20h или 40h соответственно и не будет изменен с помощью вызова PUSH.
После нажатия со значениями в неправильном положении и указателем в неправильном месте вызов полностью завершится ошибкой.
Итак, я попытался исправить эти ошибки, выполнив следующие действия. Однако в этом процессе я снова совершил роковую ошибку:
LEA RCX, TextTestfilePath
MOV RDX, 80000000h ; GENERIC_READ
MOV R8, 00000001h ; FILE_SHARE_READ
MOV R9, 0h ; NULL
MOV [RSP + 20h], 3
MOV [RSP + 28h], 80h
MOV [RSP + 36h], 00h
SUB RSP, 20h
CALL CreateFileW
ADD RSP, 20h
Здесь есть две основные ошибки, и эта должна быть более очевидной.
Я помещал значения на вершину стека. Однако при этом он полностью переопределяет наше теневое пространство с тремя аргументами. Затем я перемещал указатель стека, полностью удаляя его из только что переданных аргументов.
В 20, 28 и 36 часов я неправильно вычислял. Я добавлял 8 в десятичном виде (20+8=28, 28+8=36), однако я должен был добавлять 8 в шестнадцатеричном (20h+8h=28h, но 28h+8h != 36h, но 30h).
Ассемблер неправильно обрабатывает [RSP+28h]. Вместо этого было важно указать размер значения, которое я перемещал и вызывал указатель. Таким образом, мне нужно было добавить QWORD PTR перед ним. (Примечательно, что я на x64, поэтому я использовал QWORD вместо DWORD, так как почти все примеры MASM там пытаются и считают правильными).
После того, как я решил эти проблемы, мой код привел к следующему:
LEA RCX, TextTestfilePath
MOV RDX, 80000000h ; GENERIC_READ
MOV R8, 00000001h ; FILE_SHARE_READ
MOV R9, 0h ; NULL
SUB RSP, 20h
MOV QWORD PTR [RSP + 20h], 3
MOV QWORD PTR [RSP + 28h], 80h
MOV QWORD PTR [RSP + 30h], 00h
CALL CreateFileW
ADD RSP, 20h
Этот код делает следующее:
Как и прежде, он перемещает первые четыре аргумента в регистры.
Он перемещает указатель стека (который, как объяснялось ранее, является вершиной стека) на 20 часов, что выравнивает его посредством выравнивания по 16 байтам для 32 байтов теневого пространства. Важно отметить, что это само по себе не создает теневое пространство. Хотя он открывает 32 байта пространства, важно, чтобы мы не переопределяли 32 байта, которые мы только что открыли. Ваши аргументы не идут в этом пространстве.)
Он помещает аргументы в наш недавно измененный указатель стека, но смещает их на 20h, чтобы избежать переопределения теневого пространства.
И да, если вы видите то же, что и я, этот код на самом деле то же самое, что и этот:
MOV QWORD PTR [RSP], 3
MOV QWORD PTR [RSP + 8h], 80h
MOV QWORD PTR [RSP + 10h], 00h
SUB RSP, 20h
Это делает то же самое, но помещает аргументы в стек, прежде чем разрешить теневое пространство.
Я предпочитаю синтаксис +20h для учета теневого пространства, так как для меня более очевидно, что мы его учитываем. Но я хочу, чтобы вы из этого вынесли то, что документация для стека ужасна.
Как отметил @RaymondChen в комментариях, я не принимал во внимание эпилог и пролог для своей функции. RSP не следует модифицировать (среди нескольких других регистров, то есть RBX, RBP, RDI, RSI, RSP и с R12 по R15) внутри тела функции. Если они изменены, они должны быть сохранены и восстановлены до и после вызова функции соответственно. Это цель эпилога и пролога, наряду с отладкой при возникновении исключения.
Обновленный вызов функции делает то же самое, что и раньше, но не изменяет указатель стека:
LEA RCX, TextTestfilePath
MOV RDX, 80000000h ; GENERIC_READ
MOV R8, 00000001h ; FILE_SHARE_READ
MOV R9, 0h ; NULL
MOV QWORD PTR [RSP + 20h], 3
MOV QWORD PTR [RSP + 28h], 80h
MOV QWORD PTR [RSP + 30h], 00h
CALL CreateFileW
Я обновил «стандарт» ниже.
Вот фактический стандарт использования стека x64, которому необходимо следовать при вызове функции Win32 в MASM x64:
32 bytes
или (20h
) из указателя стека в дополнение к другим локальным переменным и аргументам стека. Пример приведен ниже.RCX
, RDX
, R8
и R9
для ARG1
, ARG2
, ARG3
и ARG4
соответственно.MOV QWORD PTR [RSP+20h], ARG5
, MOV QWORD PTR [RSP+28h], ARG6
, MOV QWORD PTR [RSP+30h], ARG7
и т. д.).CALL
ваш метод Win32.Пример правильного вызова функции Win32 показан ниже:
INCLUDELIB kernel32.lib
.CODE
main PROC
LOCAL LocalVariable: QWORD
; Prolog
PUSH RBP ; Store the RBP to restore it after
MOV RBP, RSP ; Move the RSP into RBP for debugging
SUB RSP, 40h ; 20h of shadow space for function calls
; 8h for the one local QWORD variable
; 18h for 3 stack arguments
MOV RCX, ARG1 ; Put ARG1 into RCX
MOV RDX, ARG2 ; Put ARG2 into RDX
MOV R8, ARG3 ; Put ARG3 into R8
MOV R9, ARG4 ; Put ARG4 into R9
MOV QWORD PTR [RSP + 20h], ARG5 ; Put ARG5 into RSP+20h
MOV QWORD PTR [RSP + 28h], ARG6 ; Put ARG6 into RSP+28h
MOV QWORD PTR [RSP + 30h], ARG7 ; Put ARG7 into RSP+30h
CALL MyWin32Function
; Technically, you don't need a prolog if your next
; call is going to end the process. I provide it
; for an example.
; Epilog
ADD RSP, 40h ; Same value as epilog
MOV RSP, RBP ; Restore original stack pointer
POP RBP ; Restore original RBP
RET
main ENP
END
Это гарантирует, что когда вы сохраняете свои аргументы (в RSP+20h), они все еще находятся в вашем эпилоге и прологе (то есть от RSP до RSP+40h пространства).
Вы также должны выполнить эту методологию эпилога и пролога для любых функций, которые вы можете разработать или создать. Это позволяет избежать необходимости выделять 20 часов пространства стека для каждого вызова функции и правильно обрабатывает обработку исключений Win32 для соглашения __fastcall, чтобы он (и вы) могли «пройтись по стеку».
Надеюсь, это поможет кому-то понять это немного лучше.
Я не уверен, почему стандарты выражают вещи с точки зрения справа налево, или спереди назад, или сверху вниз, потому что это объяснение неинтуитивно и субъективно в зависимости от того, как вы просматриваете стек. Использование таких терминов, как ДОБАВИТЬ или ВЫЧИТАТЬ, имеет гораздо больше смысла и является универсальным независимо от того, как отображается стек.
Я надеюсь, что это поможет кому-то избежать 6-7 часов исследований и боли, через которые прошел я, и поможет намного лучше объяснить стек! Если у кого-то есть какие-либо комментарии относительно моего объяснения вещей, которые я, возможно, упустил из виду или объяснил неправильно, пожалуйста, дайте мне знать. Однако до сих пор это работало для меня в 100% случаев.
Вы не должны изменять rsp вне пролога и эпилога. Если вы это сделаете, вам придется объявить дополнительные коды очистки, чтобы система могла пройти по стеку в случае исключения или сигнала.
@RaymondChen Я немного запутался, но я пытаюсь понять пролог и эпилог. Должен ли я также настраивать это в своей «основной» функции? Так как это то, где это находится. Не могли бы вы объяснить немного больше, что мне нужно сделать, чтобы решить эту проблему в моем коде, который я опубликовал?
Для кода x64 требуются дополнительные метаданные, известные как «коды очистки», которые генерируются специальными директивами. Подробности. Если вы перемещаете указатель стека за пределы пролога или эпилога и у вас нет указателя фрейма, вам нужно создать код раскрутки, чтобы операционная система знала, как пройти по стеку в случае исключения или сигнала. Единственные случаи, которые я могу придумать навскидку, когда вам нужно будет переместить указатель стека в середину функции, — это alloca
и сжатие, ни один из которых не применим к вашей функции.
@RaymondChen После некоторых исследований я думаю, что понял. Не могли бы вы взглянуть на мой код и проверить, правильно ли я настроил эпилоги и прологи? github.com/FireController1847/masmtest/blob/…
Я больше не могу редактировать свой комментарий, но может быть лучше дать ссылку на мастер, так как я также внес некоторые окончательные изменения. Я обновил ответ, чтобы учесть эпилог и пролог, пожалуйста, дайте мне знать, если я что-то еще пропустил. Спасибо! github.com/FireController1847/masmtest/blob/master/src/FileTest/…
Я до сих пор не вижу никаких кодов размотки. Например, после push rbp
должен быть .pushreg rbp
.
@RaymondChen Насколько я понимаю, подобные директивы применяются только к функциям FRAME, я ошибаюсь?
«Каждая функция, которая выделяет пространство стека, вызывает другие функции, сохраняет энергонезависимые регистры или использует обработку исключений, должна иметь пролог, пределы адресов которого описаны в данных раскрутки, связанных с соответствующей записью в таблице функций». Также обратите внимание, что mov rsp, rbp
не является юридической инструкцией в эпилоге.
@RaymondChen Знаете ли вы пример программы, которую я вижу для справки, написанной для MASM x64, которая делает все, что вы говорите, правильно? Потому что очевидно, что я либо не понимаю, либо документация написана плохо — нигде я не читал, не нашел и даже не видел в своих исследованиях эту часть соглашения о вызовах x86, кроме упомянутого там единственного абзаца. Нигде я не нашел описания того, как мне нужно делать эти вещи. Я действительно ценю понимание, но я устал от того, чтобы идти в погоню за дикими гусями в поисках деталей, а затем собирать воедино, каким может быть фактический ответ.
Ни один профессиональный программист не пишет всю программу x86-64 на ассемблере. Я не знаю ни одного примера. В лучшем случае короткая функция или две написаны на ассемблере. Полный пример короткой функции приведен в документации, на которую я уже ссылался.
@RaymondChen Я ценю ваши отзывы и мнения и при всем уважении не согласен. Это не место для обсуждения того, должен ли я это делать, поэтому, если у вас нет дополнительных образовательных ресурсов или примеров, которыми можно было бы поделиться со мной, я был бы признателен, если бы мы продолжили обсуждение насущного вопроса. Спасибо!
После прочтения прошу прощения за тон моих замечаний. Я не имел в виду "Вы не имеете права этим заниматься". Я имел в виду: «Вы не найдете примеров этого, потому что никто этого не делает». Примеры написания отдельных функций на ассемблере, потому что это то, что люди на самом деле делают. Теперь вы можете экстраполировать это на всю программу, поскольку main
сама по себе является просто функцией, но я не думаю, что вы найдете полностью аннотированную end-to-end «программу, полностью написанную на ассемблере, которая является производственной». готов», потому что никто этого не делает.
@RaymondChen Я действительно очень ценю разъяснение! Прошу прощения, если мое тоже показалось неуважительным. Я ценю обратную связь, честно говоря, это одна из причин, по которой я сделал этот пост. Я хочу внести больший вклад в эту область из-за значительной нехватки подробной документации и знаний о MASM x64. Я определенно продолжу свои исследования, чтобы попытаться найти больше информации по этому вопросу, надеюсь, я смогу получить дополнительные разъяснения по процессу раскрутки. Я был расстроен прошлой ночью, но это не оправдание моей реакции.
ваши параметры в неправильном месте в стеке. 5-й должен быть в [rsp+20h], 6 - [rsp+28h] и так далее. но вы используете
push
что не так