Win32 CALL для CreateFileW всегда возвращает ERROR_INVALID_PARAMETER в MASM x64. Почему?

В настоящее время я пытаюсь изучить 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 я все еще не могу найти источник проблемы. Я проверил следующее:

  • Каждый из параметров правильный.
  • TextTestfilePath правильно записан в память (и успешно выполнен предыдущий вызов GetFileAttributesW)
  • Насколько я понимаю, параметры RDX, R8 и первого стека (последний толчок) верны.
  • Я считаю, что правильно распределяю теневое пространство, необходимое для успешного вызова.

Я все еще изучаю MASM x64 с ограниченной информацией о нем, но у меня есть общее понимание того, как все это работает, и я прочитал несколько книг об этом и использовал часть Win32 Console API до эта точка.

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

Прежде чем комментировать, что я делаю что-то не так, пожалуйста, помогите не только мне, но и сообществу найти хорошо документированные источники для изучения MASM x64, которые могут хорошо объяснить эту концепцию! Простое высказывание «Вы делаете что-то не так, и вам нужно это исправить» не помогает решить эту проблему и не способствует обсуждению, которое поощряет обучение и образование, чего можно было бы ожидать от такого сайта, как StackOverflow. Ссылки на сторонние источники, в дополнение к очевидным документам Microsoft, невероятно полезны для общего обзора того, что ожидается, вместо того, чтобы предполагать, что определенные вещи известны, когда они могут быть неизвестны.

ваши параметры в неправильном месте в стеке. 5-й должен быть в [rsp+20h], 6 - [rsp+28h] и так далее. но вы используете push что не так

RbMm 17.06.2023 12:42

@RbMm Наверное, я должен был отметить, я также пробовал SUB RSP, 20h; SUB RSP, 50h и различные другие позиции стека безрезультатно. Я не нашел хороших ресурсов, чтобы научить меня соглашениям по программированию MASM x64, поэтому я собирал по кусочкам то, что мог, где мог. Я был незнаком с синтаксисом MOV [RSP+20h], значение. Хотя в документации Windows мало что говорится о точных позициях значений в стеке, по крайней мере, из того, что мне удалось найти. Я уверен, что где-то что-то пропустил. Если у вас есть какие-либо ресурсы, которые могли бы указать мне правильное направление, это было бы невероятно полезно!

FireController1847 17.06.2023 12:45

learn.microsoft.com/en-us/cpp/build/stack-usage?view=msvc-17‌​0

RbMm 17.06.2023 12:49
CMP EAX, -1 также формально не правильно, несмотря на то, что всегда будет работать нормально. должно быть CMP RAX, -1. вместо этого CALL CreateFileW лучший пользователь CALL __imp_CreateFileW
RbMm 17.06.2023 12:53

@RbMm Я ценю понимание. Я не был знаком с нотацией __imp_ для доступа, поэтому я все определял вручную. Я использую PROTO в коде. Я много раз читал эту страницу стека, но, должно быть, у меня не выходит из головы, потому что я понятия не имею, как то, что там упоминается, переводится в MOV [RSP+20h], VAL. Лучшее из того, что он упоминает, - это «поместить аргументы в стек», повторенное три раза, без упоминания о том, что это за смещение или как далеко оно должно идти. Из того, что я узнал через ChatGPT, это 20-часовая тень для первых 4, +8 для каждого аргумента стека, выровненная по ближайшим 16

FireController1847 17.06.2023 12:57

СИЛЬНОЕ ПРЕДЛОЖЕНИЕ: 1) Напишите программу на C, которая вызывает Win32 API "CreateFile()". 2) Разобрать: cl /FAs mytest.c

paulsm4 19.06.2023 00:02

Я ценю понимание @paulsm4! Я нашел серию, которая делает то же самое с их примером функции добавления, вы бы сказали, что это похоже на то, что получается из этого? medium.com/@sruthk/…

FireController1847 19.06.2023 00:03
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я понял это. Я действительно неправильно нажимал на стек. Но у меня было фундаментальное непонимание того, как работает стек, что документация Microsoft проделала ужасную работу по объяснению.

Что я сделал не так

Попытка №1

Как указал @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
  1. Я модифицировал указатель стека, чтобы вытолкнуть теневое пространство. Это правильно, и 20h является правильным значением для этого, потому что это 32 байта теневого пространства, которое переводится в 20h в шестнадцатеричном формате. Это сохранит все 16-битное выравнивание.

  2. Я помещал аргументы в стек. Проблема в том, что я делал это неправильно (или наоборот). RSP или указатель стека ссылается на вершину стека. Когда я PUSH помещал значения в стек, он помещал значения выше в стек. В довершение всего он изменит указатель стека так, чтобы он больше не был выровнен по 16 битам. Ожидается, что указатель стека будет иметь значение 20h или 40h соответственно и не будет изменен с помощью вызова PUSH.

  3. После нажатия со значениями в неправильном положении и указателем в неправильном месте вызов полностью завершится ошибкой.

Попытка №2

Итак, я попытался исправить эти ошибки, выполнив следующие действия. Однако в этом процессе я снова совершил роковую ошибку:

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

Здесь есть две основные ошибки, и эта должна быть более очевидной.

  1. Я помещал значения на вершину стека. Однако при этом он полностью переопределяет наше теневое пространство с тремя аргументами. Затем я перемещал указатель стека, полностью удаляя его из только что переданных аргументов.

  2. В 20, 28 и 36 часов я неправильно вычислял. Я добавлял 8 в десятичном виде (20+8=28, 28+8=36), однако я должен был добавлять 8 в шестнадцатеричном (20h+8h=28h, но 28h+8h != 36h, но 30h).

  3. Ассемблер неправильно обрабатывает [RSP+28h]. Вместо этого было важно указать размер значения, которое я перемещал и вызывал указатель. Таким образом, мне нужно было добавить QWORD PTR перед ним. (Примечательно, что я на x64, поэтому я использовал QWORD вместо DWORD, так как почти все примеры MASM там пытаются и считают правильными).

Попытка №3

После того, как я решил эти проблемы, мой код привел к следующему:

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

Этот код делает следующее:

  1. Как и прежде, он перемещает первые четыре аргумента в регистры.

  2. Он перемещает указатель стека (который, как объяснялось ранее, является вершиной стека) на 20 часов, что выравнивает его посредством выравнивания по 16 байтам для 32 байтов теневого пространства. Важно отметить, что это само по себе не создает теневое пространство. Хотя он открывает 32 байта пространства, важно, чтобы мы не переопределяли 32 байта, которые мы только что открыли. Ваши аргументы не идут в этом пространстве.)

  3. Он помещает аргументы в наш недавно измененный указатель стека, но смещает их на 20h, чтобы избежать переопределения теневого пространства.

И да, если вы видите то же, что и я, этот код на самом деле то же самое, что и этот:

MOV QWORD PTR [RSP], 3
MOV QWORD PTR [RSP + 8h], 80h
MOV QWORD PTR [RSP + 10h], 00h
SUB RSP, 20h

Это делает то же самое, но помещает аргументы в стек, прежде чем разрешить теневое пространство.

Я предпочитаю синтаксис +20h для учета теневого пространства, так как для меня более очевидно, что мы его учитываем. Но я хочу, чтобы вы из этого вынесли то, что документация для стека ужасна.

Попытка №4

Как отметил @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 (в лучшем виде)

Вот фактический стандарт использования стека x64, которому необходимо следовать при вызове функции Win32 в MASM x64:

  1. В начале вашей функции (включая main) настройте пролог.
    • В этом прологе вы выделяете 20 часов теневого пространства для вызовов функций, вычитая 32 bytes или (20h) из указателя стека в дополнение к другим локальным переменным и аргументам стека. Пример приведен ниже.
  2. Назначьте свои первые четыре аргумента RCX, RDX, R8 и R9 для ARG1, ARG2, ARG3 и ARG4 соответственно.
  3. Поместите оставшиеся аргументы в стек без изменения указателя стека и за 20 часов зарезервированного пространства (то есть MOV QWORD PTR [RSP+20h], ARG5, MOV QWORD PTR [RSP+28h], ARG6, MOV QWORD PTR [RSP+30h], ARG7 и т. д.).
  4. CALL ваш метод Win32.
  5. В конце вашей функции и после завершения всех вызовов (включая основной) настройте эпилог.
    • В этом эпилоге вы восстанавливаете стек до исходного указателя до вызова функции. Вы добавите то же значение, которое вы вычли в начале функции.

Пример правильного вызова функции 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 вне пролога и эпилога. Если вы это сделаете, вам придется объявить дополнительные коды очистки, чтобы система могла пройти по стеку в случае исключения или сигнала.

Raymond Chen 18.06.2023 05:01

@RaymondChen Я немного запутался, но я пытаюсь понять пролог и эпилог. Должен ли я также настраивать это в своей «основной» функции? Так как это то, где это находится. Не могли бы вы объяснить немного больше, что мне нужно сделать, чтобы решить эту проблему в моем коде, который я опубликовал?

FireController1847 18.06.2023 05:15

Для кода x64 требуются дополнительные метаданные, известные как «коды очистки», которые генерируются специальными директивами. Подробности. Если вы перемещаете указатель стека за пределы пролога или эпилога и у вас нет указателя фрейма, вам нужно создать код раскрутки, чтобы операционная система знала, как пройти по стеку в случае исключения или сигнала. Единственные случаи, которые я могу придумать навскидку, когда вам нужно будет переместить указатель стека в середину функции, — это alloca и сжатие, ни один из которых не применим к вашей функции.

Raymond Chen 18.06.2023 07:18

@RaymondChen После некоторых исследований я думаю, что понял. Не могли бы вы взглянуть на мой код и проверить, правильно ли я настроил эпилоги и прологи? github.com/FireController1847/masmtest/blob/…

FireController1847 18.06.2023 23:19

Я больше не могу редактировать свой комментарий, но может быть лучше дать ссылку на мастер, так как я также внес некоторые окончательные изменения. Я обновил ответ, чтобы учесть эпилог и пролог, пожалуйста, дайте мне знать, если я что-то еще пропустил. Спасибо! github.com/FireController1847/masmtest/blob/master/src/FileT‌​est/…

FireController1847 19.06.2023 00:10

Я до сих пор не вижу никаких кодов размотки. Например, после push rbp должен быть .pushreg rbp.

Raymond Chen 19.06.2023 01:23

@RaymondChen Насколько я понимаю, подобные директивы применяются только к функциям FRAME, я ошибаюсь?

FireController1847 19.06.2023 01:35

@RaymondChen Знаете ли вы пример программы, которую я вижу для справки, написанной для MASM x64, которая делает все, что вы говорите, правильно? Потому что очевидно, что я либо не понимаю, либо документация написана плохо — нигде я не читал, не нашел и даже не видел в своих исследованиях эту часть соглашения о вызовах x86, кроме упомянутого там единственного абзаца. Нигде я не нашел описания того, как мне нужно делать эти вещи. Я действительно ценю понимание, но я устал от того, чтобы идти в погоню за дикими гусями в поисках деталей, а затем собирать воедино, каким может быть фактический ответ.

FireController1847 19.06.2023 06:56

Ни один профессиональный программист не пишет всю программу x86-64 на ассемблере. Я не знаю ни одного примера. В лучшем случае короткая функция или две написаны на ассемблере. Полный пример короткой функции приведен в документации, на которую я уже ссылался.

Raymond Chen 19.06.2023 14:27

@RaymondChen Я ценю ваши отзывы и мнения и при всем уважении не согласен. Это не место для обсуждения того, должен ли я это делать, поэтому, если у вас нет дополнительных образовательных ресурсов или примеров, которыми можно было бы поделиться со мной, я был бы признателен, если бы мы продолжили обсуждение насущного вопроса. Спасибо!

FireController1847 19.06.2023 23:50

После прочтения прошу прощения за тон моих замечаний. Я не имел в виду "Вы не имеете права этим заниматься". Я имел в виду: «Вы не найдете примеров этого, потому что никто этого не делает». Примеры написания отдельных функций на ассемблере, потому что это то, что люди на самом деле делают. Теперь вы можете экстраполировать это на всю программу, поскольку main сама по себе является просто функцией, но я не думаю, что вы найдете полностью аннотированную end-to-end «программу, полностью написанную на ассемблере, которая является производственной». готов», потому что никто этого не делает.

Raymond Chen 20.06.2023 00:03

@RaymondChen Я действительно очень ценю разъяснение! Прошу прощения, если мое тоже показалось неуважительным. Я ценю обратную связь, честно говоря, это одна из причин, по которой я сделал этот пост. Я хочу внести больший вклад в эту область из-за значительной нехватки подробной документации и знаний о MASM x64. Я определенно продолжу свои исследования, чтобы попытаться найти больше информации по этому вопросу, надеюсь, я смогу получить дополнительные разъяснения по процессу раскрутки. Я был расстроен прошлой ночью, но это не оправдание моей реакции.

FireController1847 20.06.2023 00:19

Другие вопросы по теме