Что такое консольное приложение win32

Обновлено: 07.07.2024

Каждый второй опытный пользователь, услышав от новичка фразу "А что такое консольная программа?", начинает скрежетать зубами. Поэтому если вы - новичок, то лучше прочитать эту статью, чтобы не напороться на этого второго из двух опытных, а если вы уже опытный пользователь, то имеет смысл подсунуть заметку новичку, чтобы тот нашёл ответ на свой вопрос.

Итак, что же скрывается за этим страшным словосочетанием - консольное приложение? Приложение, как вы знаете, это в мире Windows не то, что к чему-то прикладывается, а компьютерная программа. Word, Excel, пасьянс "Косынка" и Internet Explorer - это всё приложения. Приложения бывают разными. Не только в том смысле, что Word отличается от "Блокнота", но и по принципу организации своего пользовательского интерфейса. Интерфейс - это внешний вид программы, и в Windows он бывает двух типов. Первый - это графический, второй - текстовый. Графический интерфейс система подсовывает вам под нос ежедневно. Все программы, имеющие красивые разноцветные окна, имеют и графический интерфейс. Даже Word и "Блокнот", хоть и работают с текстом, но интерфейс имеют графический. Где же тогда в наши дни можно увидеть программу, работающую в режиме текстового интерфейса?

Дело в том, что ещё не так давно, около двадцати лет назад, мощности компьютеров, стоящих у пользователей дома и на работе, не хватало на отображение даже такой несложной графики, как окна (что и говорить о трёхмерных видеоиграх!). Поэтому компьютеру и пользователям приходилось общаться с помощью текста. Пользователь вводил специальные команды - например, команда dir в первой из систем производства Microsoft, DOS, позволяла просмотреть список файлов и папок в определённой директории, а команда ver показывала версию операционной системы, с которой работал пользователь. Программы, которые запускал пользователь, тоже, естественно, работали в текстовом режиме.

Командная строка, как вы видите, сохранилась в Windows до сих пор. Команды, которые можно выполнить из неё, подробно описаны в справке Windows. А приложения, которые выполняются в текстовом режиме, теперь называются консольными.

Почему же командная строка продолжает жить, несмотря на свой почтенный возраст? Во-первых, есть множество программ, которым не нужен графический интерфейс - например, программы по взлому паролей, которые просто подбирают комбинации символов, пока то, что закрыто паролем, не откроется. Кроме того, командная строка приходит на помощь тем пользователям, которые выучили назубок её команды и быстро набирают с клавиатуры, - для них она гораздо более быстрый способ работать с компьютером, чем графический интерфейс.

Консольных программ масса, и они используются часто для того, чтобы упростить рутинные действия пользователя. Дело в том, что с ними можно обращаться точно так же, как и с обычными командами. А те, в свою очередь, можно записать в специальный текстовый файл с расширением BAT или CMD (такой файл называется командным), и их можно потом выполнить все залпом как обычную программу - достаточно в "Проводнике" дважды кликнуть по этому файлу мышью.

7.2 Консольное приложение - Win32 Console Application

  1. Запустите MSVisualC++6.0
  2. Щелкните меню File->New->Win32 Console Application.
  3. Выберете каталог и имя проекта задайте glaux, щелкните OK.
  4. Выберете An Empty Project, щелкните Finish.
  5. Создайте новый текстовый файл и сохраните его с именем glaux.c.
  6. Присоедините его к проекту. Project->Add To Project->Files
  7. Щелкните Build->Set Active Configuration и установите тип проекта glaux - Win32 Release
  8. Далее щелкаете Project->Settings->Link->Object/library modules: и добавьте туда opengl32.lib, glu32.lib и glaux.lib

Проект у нас теперь есть, давайте писать glaux.c. Файл, в котором находится исходный код программы, желательно начинать с комментария. Это необязательно, но этого требует хороший стиль. В комментариях можно указать имя автора, способ связи - обычно, адрес электронной почты. Далее, можно кратко описать, что находится в этом файле. Неплохо вести некоторый дневник здесь же: что и когда вы добавили. У меня эти комментарии выглядят так:

Теперь надо включить заголовочные файлы:

Давайте напишем функцию main(). Я посчитал, что наиболее правильно и понятно будет дать код и прокомментировать его подробно. У меня функция main() выглядит так:

Вот и все с функцией main(). Осталось написать код функции resize и функции display. Вставьте следующий код перед функцией main().

Здесь нужно сделать пояснения по поводу glMatrixMode. Функции glOrtho и glFrustum работают с матрицей, отвечающей за тип проекции. Они просто загружают соответствующую матрицу. Вы можете установить свой тип проекции, если вам это понадобится. Сначала вы говорите, что будете изменять матрицу проекции - glMatrixMode с параметром GL_PROJECTION. Потом, с помощью glLoadMatrix загружаете соответсвующую матрицу. Функции glTranslate/glRotate работают с матрицей вида. Ее мы загружаем последней строкой - glMatrixMode( GL_MODELVIEW ).
В параметрах контекста воспроизведения мы установили AUX_DOUBLE. Это значит, что рисоваться все будет сначала в буфер. Для того, что бы скопировать содержимое буфера на экран, вызывается функция auxSwapBuffers(). Если вы программировали анимацию для MSDOS или Windows, то наверняка использовали такой прием, чтобы избавиться от мерцания на экране. В функции display мы сначала очищаем буфер. Цвет, которым заполняется буфер при очищении, можно установить в функции main() вызовом glClearColor(r,g,b).



Исходный файл смотрите здесь. Исполняемый файл здесь.

Немного терминологии. Называть эту статью введением в Win32 было бы неверно — консольное приложение под Windows такое же полноправное Win32 приложение. Что такое консольное приложение? Ах да - да ты ведь регулярно сталкиваешься с такими программами - например мой любимый файл-менеджер FAR является полноценным консольным Win32 приложением. Точка входа для консольных приложений та же, что и в дос программах на Си - это функция main(). Однозначно разделять оконные приложения и консольные тоже, как мне кажется, было бы не правильно - программа, использующая как точку входа в программу WinMain(), как это ни странно, может и не создавать окно, а даже организовать свою консоль (!) вызовом AllocConsole(), а консольное приложение без проблем может создать окно. Возникает лишь один резонный вопрос: зачем это нужно? ;) Консольные приложения хороши своей самодостаточностью — они идеально подходят для утилит командной строки. О достоинствах оконных приложений я говорить не буду - чуть ниже мы перейдем напрямую к оконным приложениям под Windows.

Архитектурно, эти обе категории различаются стабом (stub) который прилинковывается к твоему откомпилированному коду - в данном контексте под стабом подразумевается некий программный код, выполняемый до (и по выходу) передачи управления в main() или WinMain() (в зависимости от типа приложения). Стабы различны для каждых фирм и часто для разных версий одного и того же продукта - что вполне логично. Например, стаб Борланда не совпадает со стабом от MS, хотя обычно они выполняют одни и те же цели - инициализацию каких-либо внутренних обработчиков и т.п. Исходные тексты стаба (кстати говоря, обычно открыты и исходные тексты рантайм функций - например, вполне реально найти исходный текст знакомых тебе fopen() или printf()) открыты. Например, при инсталяции Visual C++ ты можешь установить и исходные тексты стаба и рантайма (по умолчанию в \CRT\SRC).

Ну что - перейдем к созданию оконных приложений? Как я уже и сказал точка входа в оконные приложения WinMain(). Да - чуть не забыл - пример, и исходные тексты лежат ЗДЕСЬ. Приложение максимально упрощено - не жди от него чего-либо необычного. Наша цель понять общие идеи.

Итак, WinMain(). Давай разберемся с его определением. Ну, то что функция возвращает int, это понятно (кстати она возвращает его всегда - переопределить возвращаемое значение скажем на void не выйдет - компилятор выдаст ошибку) - но вот что такое WINAPI? Опытный читатель уже успел (если сталкивался с подобным) перерыть заголовочные *.h файлы Windows ("хедеры") в поисках макроопределения WINAPI. Определение находится в windef.h. Реально то, что подставляется вместо WINAPI, зависит от используемой платформы (ОС) и версии компилятора. Так что же это такое? Это СОГЛАШЕНИЯ о вызове функции. Что это такое? Дело в том, что любая программа высокого уровня (конечно же речь идет о компиляторах, а не интерпретаторах) транслируется при компиляции в маш-коды понятные процессору. Сейчас тебе понадобится минимальные знания процессоров Intel архитектуры IA-32 (достаточно даже более ранней). В ассемблере (язык в котором одна команда сопоставима одной комманде машинных кодов - в общем-то, те же машинные коды, но в понятной для человека форме) нет вызова функции с параметрами, как и возврата результата - зато есть стек, регистры, безусловный переход JMP и вызов подпрограммы инструкцией CALL. Какой код сгенерирует компилятор - зависит от него самого - я не буду затрагивать эту обширную тему - скажу лишь, что существует некоторое количество соглашений, благодаря которым компилятор генерирует код для вызова функций - вот некоторые из них (Кстати говоря - программа, написанная на ассемблере вручную не обязательно придерживается каких-то соглашений. А зачем? Программист легко может использовать необходимые ему регистры - потому, что он пишет свои функции сам. Другое дело, когда он использует какие-то стандартные библиотеки - теперь он ограничен спецификацией используемого им API):

* __stdcall - параметры передаются через стек - справа налево (в обратном порядке - первый параметр ложится в стек последним). Вызываемая процедура "возвращает" указатель стека ESP "на место". Большинство Win32 функций используют эти соглашения.

* __cdecl - параметры передаются через стек, справа налево. Вызывающая функция (та, из которой был инициирован вызов) "правит" стек. Классический вызов в Си.

Для полного понимания приведу пример того, как бы скорее всего ассемблировался вызов следующей функции используя соглашения __cdecl:

ParseString("Это просто строка !",0x12);

А теперь ассемблерные текст:

Это что касается соглашений - WINAPI - определяется одним из этих типов (не из всех, конечно же, - из пары-тройки) в зависимости, как я уже говорил, от платформы или используемого компилятора. Параметры у WinMain():

* hInstance - это идентификатор экземпляра приложения. Не пугайся типа HINSTANCE. Это наверняка какой-нибудь unsigned int. Каждому приложению присваивается уникальный номер, который стаб читает вызовом GetModuleHandle(), а потом передает его тебе. Если ты в приложении прочитаешь этой функцией идентификатор приложения - ты увидишь, что он совпадает с тем, что тебе передали в WinMain(). Этот идентификатор пригодится нам для очень многих целей.

* hPrevInstance - это идентификатор предыдущего экземпляра приложения. Под Win32 он всегда равен NULL. Тут уж тебе решать - работать дальше или нет - в своем примере фактически я отказываюсь работать, если второй параметр отличен от NULL. Кстати - дизассемблируй Quake1,Quake2 или Quake3 - увидишь то же самое.

* lpCmdLine - просто указатель на командную строку - без разделения на параметры - одна монолитная строка.

* nCmdShow - это параметр описывает флажки для окна - то, как оно будет показано на экране при создании. Обычно этот параметр передают в вызов ShowWindow(). В своем приложении я его не использую, а просто передаю в ShowWindow() флажок SW_SHOW.

Этот кусочек кода отвечает за регистрацию класса окна - да, именно - скорее это даже класс окна.

В этой статье я откоментировал код для того, чтобы не тратить много времени на описание и так понятных действий. Пара оговорок - идентификатор ресурсов - некоторая константа, описывающая номер ресурса - в моем приложении нет своих ресурсов - если бы они были - можно было бы использовать их - указав при загрузке идентификатор экземпляра приложения, вторым - идентификатор ресурса. Продолжаем?

Учти, что эту программку я писал, отталкиваясь исключительно от MS Visual C, то есть не пользовался другими компиляторами. Хотя, думаю, проблем не должно возникнуть. Наверное, этого достаточно для начала - теперь дело только за тобой. Удачной «охоты»!

Рустэм Галеев aka Roustem

5. Консольное приложение

Существует разновидность приложений Windows, которые называются консольными. По своим "внешним" проявлениям они напоминают приложения DOS, запущенные в Windows. Тем не менее, это настоящие Win32-приложения, которые под DOS работать не будут; для них также доступен Win32 API, а кроме того, они могут использовать консоль - окно, предоставляемое системой, которое работает в текстовом режиме и в которое можно вводить данные с клавиатуры.

Особенность консольных приложений в том, что они работают не в графическом, а в текстовом режиме. Для этого используются унаследованые от DOS так называемые стандартный ввод и стандартный вывод. Все, что пользователь вводит с клавиатуры (когда консольное окно имеет фокус), попадает в буфер стандартного ввода, откуда данные можно читать, как из файла. Выходные же данные можно записать, как в файл, в буфер стандартного вывода, и они будут отображены в консольном окне.

Еще одной особенностью стандартных ввода и вывода является возможность их перенаправления в файл. Этим мы уже пользовались при создании наших приложений, используя в командной строке знаки '<' для перенаправления ввода и '>' для перенаправления вывода. Debug, будучи приложением DOS, использует для приема команд стандартный ввод, а для отображения данных - стандартный вывод. Когда мы писали: 'debug < code.txt', содержащиеся в файле code.txt данные поступали в debug так, как будто их набирают на клавиатуре. При команде же 'debug > result.lst' вся информация, которая выводилась бы на экран, попадала в файл 'result.lst'.

Из приложений, подобных debug, т.е. использующих для ввода данных стандартный ввод, а для вывода - соответственно стандартный вывод, можно строить даже своеобразные "конвейеры". Для этого данные из стандартного вывода одного приложения подают на стандартный ввод другого посредством специального знака командной строки '|' (вертикальной черты). В команде

данные проходят последовательную обработку этими тремя приложениями. Например, приложение1 могло бы составлять список из данных источника, приложение2 - сортировать его, а приложение3 - форматировать нужным образом. Таким образом, у консольных приложений есть свои преимущества; их удобно использовать в качестве "строительных блоков" для автоматизации многих рутинных задач, не требующих интерактивного взаимодействия с пользователем.

Работа со стандартными вводом и выводом "изнутри" подобна работе с файлами. Стандартный ввод выглядит как файл с разрешением "только для чтения", а стандартный вывод - как файл с разрешением "только для записи". Для работы с ними используют соответствующие функции API - ReadFile и WriteFile из модуля Kernel32.dll. Рассмотрим их подробнее.

При вызове функции ReadFile в стек помещаются 5 параметров в следующем порядке:

  1. адрес структуры, использующейся при асинхронном вводе-выводе. При обычном (синхронном) вводе значение этого параметра равно нулю;
  2. адрес переменной (4 байта), по которому будет записано количество действительно прочитанных функцией байтов (это значение может быть меньше заявленного - например, если кончились данные);
  3. число байтов, которые нужно прочесть ("заявка");
  4. адрес, по которому нужно разместить прочитанные данные (буфер);
  5. описатель файла, из которого производится чтение.

Внимания заслуживает последний аргумент. Для работы с файлом используется так называемый описатель (handle) - это некий идентификатор, который система присваивает файлу после его открытия. На самом деле, при открытии файла создается внутренняя системная структура, в которой хранятся различные вспомогательные данные, такие как текущая позиция, с которой нужно читать или записывать данные, и т.п. Все обращения к файлам возможны только после их открытия и только по их описателям.

Функция WriteFile также принимает 5 схожих параметров:

  1. адрес структуры для асинхронного вывода;
  2. адрес переменной (4 байта), в которую будет помещено количество действительно записанных байтов;
  3. число байтов, которые нужно записать ("заявка");
  4. адрес начала буфера, где находятся предназначенные для записи данные;
  5. описатель файла, в который производится запись.

Настало время обсудить один важный вопрос. Функции не только принимают параметры, часто они еще возвращают значения. Результат работы функции по ее возвращении (т.е. перед выполнением следующей после вызова функции инструкции) оказывается в регистре EAX. Это общее соглашение: когда мы начнем создавать свои функции, мы тоже будем должны записывать в регистр EAX значение, которое должно быть возвращено как результат функции. В случае функции GetStdHandle таким результатом как раз и является нужный нам описатель. Его можно либо сохранить где-то в памяти (переписав туда значение из регистра), либо использовать прямо в регистре, если вызов нужной функции непосредственно следует после получения описателя.

Здесь нужно отметить еще один момент. Мы уже знаем, что из 8 общих регистров один (ESP) используется в качестве указателя стека, и его трогать нельзя. На самом деле, при работе со стеком используется еще и второй регистр - EBP, поэтому число доступных для манипуляций регистров сокращается до 6. Теперь задумайтесь над вопросом: а что случается с данными, которые находились в регистрах, после вызова функции? Особенно, если это "чужие" функции, являющиеся для нас "черными ящиками" (как в случае с функциями API). Значения в регистрах могут быть перезаписаны (ведь надо с чем-то работать!), а могут остаться без изменения. Чтобы внести ясность в этот вопрос, для работы с функциями Win32 API было принято следующее соглашение: при вызовах любых функций значения регистров EBX, ESI и EDI остаются без изменений - какие были перед вызовом функции, такие будут и после; значения же регистров EAX, ECX и EDX могут быть изменены произвольным образом. В регистре EAX, как мы уже знаем, будет находиться результат работы функции (если функция возвращает результат). Если функция не имеет возвращаемого результата, значение в EAX не определено.

Практический же вывод такой. Если нам нужно, чтобы значение в "изменяемых" регистрах (EAX, ECX или EDX) сохранилось после вызова функции, перед ее вызовом необходимо поместить значение соответствующего регистра в стек, а после вызова функции - извлечь его оттуда (в тот же регистр). И наоборот: если мы создаем свою функцию, которую может вызвать система (например, забегая вперед, это относится к главной функции окна), и если в работе этой функции нам приходится использовать регистры, которые не должны изменяться (EBX, ESI или EDI), мы должны в самом начале функции сохранить значение этого используемого регистра в стеке, а перед возвратом из функции - восстановить его. В случае "изменяемых" регистров этого делать не нужно.

Начнем с секции данных. Их немного - в начале секции (по смещению 2000h, который после загрузки превратится в виртуальный адрес 402000h) разместим переменную в 4 байта для вывода количества записанных байтов. Сразу за ней (по адресу 402004h) будет выводимая текстовая строка. Набираем файл data.txt:

Числа 0Ah и 0Dh после строки являются ASCII-символами перехода на новую строку; сама строка должна завершаться нулем.

Теперь надо заняться секцией импорта. Нам нужно импортировать три функции, и все из модуля Kernel32.dll: GetStdHandle, WriteFile и ExitProcess. В начале секции, как обычно, таблица импортируемых адресов (IAT); на этот раз она имеет, по числу функций, 3 поля и четвертое нулевое. Сразу вслед за IAT расположим таблицу поиска, тем более, что они при загрузке должны быть идентичны. Затем будет таблица импорта, содержащая одну запись для единственного импортируемого модуля и одну завершающую нулевую запись (общий размер 28h байт). Затем последуют строки с именами модуля и функций. Здесь удобно использовать два прохода в режиме ассемблирования - при "черновом" содержимое структур можно просто заполнять нулями (сохраняя лишь размер полей), а для второго "чистового" прохода подставить из полученного файла отчета нужные значения. Для этой же цели лучше выбрать для "сборки" образа в debug то же смещение, что и у загруженной в память секции (в данном случае - 3000h; файл rdata.txt):

Переходим к секции кода. Освежим знания по инструкциям: "короткая" команда помещения в стек 6Ah + 1 байт, "длинная" - 68h + 4 байта; команда вызова функции - опкод 0FFh, байт ModR/M 15h, указывающий, что операнд находится в памяти по 4-байтному адресу, который включен в инструкцию. Обратите внимание, что для помещения в стек числа 0FFFFFFF5h мы можем использовать "короткий" вариант инструкции со знаковым расширением, т.к. это на самом деле отрицательное число, представимое в виде 1 байта (-11 = 0F5h. Функцию WriteFile мы вызываем непосредственно после функции GetStdHandle, значит, нужный нам описатель файла будет находиться в регистре EAX; поэтому в этот раз придется использовать также инструкцию помещения в стек значения регистра EAX (если помните, это 50h). Указатели на нужные нам функции будут находиться после загрузки в соответствующих полях IAT, по адресам 403000h, 403004h и 403008h. Итак, файл code.txt:

Осталось лишь добавить PE-заголовок. Сначала скопируем его шаблон (header.txt) в рабочий каталог, а затем слегка его подправим. Потребуются изменения всего в трех местах. Самое главное - нужно изменить подсистему: вместо графической (2) поставить консольную (3). Собственно, это единственное, чем консольные приложения отличаются от графических! Находим в шаблоне строки:

Сразу после нее должно быть:

Теперь надо указать расположение таблицы импорта. Находим строку:

За ней должен следовать текст:

Наконец, мы поменяли местами секции .data и .rdata (хотя в принципе этого можно было и не делать). Находим начало второй секции:

И заменяем оставшийся текст на следующий:

Вот и все. В файле сборки (make.bat) секции также должны идти в соответствующем порядке:

Проверив файл отчета report.lst, можно запускать cnsl.exe. Если вы запускаете его не из консоли, создаваемое окно будет мелькать - закрываться сразу после завершения программы. Поэтому можно запустить сначала консоль командной строки DOS и уже из него - наше приложение, набрав его имя (и путь, если требуется).

Еще одно примечание - в консольных приложениях используется кодировка DOS. Поэтому если вы набрали текст для вывода в Блокноте и на русском, то в консольном окне прочесть его не сможете - в Windows используется другая кодировка (ANSI).

На самом деле, возможности текстового вывода шире, чем можно было бы подумать. Попробуйте в качестве примера использовать такой файл data.txt:

Я хочу знать, в чем разница между приложением Windows Form, Win32Application и консолью. Я знаю, что приложение Windows Form и приложение Win32 являются графическим инструментом, но я хочу знать, когда использовать одно приложение над другим, и могу ли я преобразовать консольное приложение для окна формы приложения?

Решение

Win32 обычно относится к 32-битному Windows API. Тем не менее _WIN32 макрос определен как для 32-битного, так и для 64-битного программирования. Как тип проекта Visual Studio, он включает в себя программы уровня API с графическим интерфейсом и консольной подсистемой.

Подсистема Windows небольшое целочисленное значение в заголовке исполняемого файла, которое сообщает Windows, какие сервисы нужны этой программе. Это значение может быть проверено, например, с помощью от Microsoft dumpbin программа, например dumpbin c:\windows\notepad.exe /headers | find "ubs" , В Windows 9x dumpbin вывод был доступен через функцию предварительного просмотра файла, но эта функция была прекращена.

Каждый процесс в Windows может быть связан с одним и не более чем одним консольным окном.

GUI подсистема означает, что Windows будет НЕ попытайтесь оборудовать каждый экземпляр соответствующим консольным окном. Однако процесс может создать само окно консоли. Обычно эта подсистема используется для обычных программ с графическим пользовательским интерфейсом (следовательно, «GUI»), а для большинства компоновщиков она указывается как «окна».

Консольная подсистема означает, что Windows будет пытаться оснастить каждый экземпляр соответствующим консольным окном, создавая новое при необходимости.

Обратите внимание, что

Тот же самый исходный код может быть собран как консоль или подсистема GUI. И это очень легко сделать. Просто измените спецификацию подсистемы.

Исполняемый файл подсистемы GUI имеет стандартные потоки, как и исполняемый файл консольной подсистемы.

Исполняемый файл консольной подсистемы может представлять графический интерфейс пользователя, так же как и графический интерфейс.

Также обратите внимание, что

Нет такой проблемы с инструментами GNU, то есть g ++.

Другие решения

«Приложение win32» является родным приложением Windows GUI.

Читайте также: