Логический 0, 1, команды Windows и все-все-все

Рассказ о том, как магия октрытие окон Windows в электрические сигналы превращает.

Недавно меня друг, не работающий в IT сфере, попросил объяснить, как же любые действия пользователя превращаются в нолики и единички, о которых рассказывают на уроках информатики. И растолковать это надо простым понятным для не технаря языком. И тут я понял, что это очень сложная задача: гораздо сложнее, чем объяснить по отдельности принцип действия отдельно взятого устройства. Поставленная задача сродни тому, чтобы рассказать принцип устройства нашей вселенной в двух словах.

В ходе разговора выяснилось, что в школах рассказывают массу разрозненных фактов, которые никак не укладываются в единую систему, и более того, слабо связаны между собой логической цепочкой. А причина тому – отсутствие этого самого понимая у самих преподавателей.

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

Итак, на одном конце у нас – нули и единицы, а на другом – операционная система. Как же происходит это гениальное преобразование? Начнем снизу, с железа.

Общее представление о компьютерной микроэлектронике

Из курса информатики мы помним, что основные узлы компьютера, без которых он не сможет работать – это процессор и оперативная память. Принцип работы компьютера заключается в оперировании блоками групп нулей и единиц между этими двумя устройствами. Существует набор стандартных операций, которые можно совершать над этими данными: сохранять (записывать), складывать, вычитать, перемещать и ряд аналогичных других. Для любых этих операций используются операнды (те данные, над которыми выполняется операция), которые сохранятся в адресованных ячейках памяти или регистрах процессора. Результат каждой операции записывается в еще одну ячейку или регистр. На этом этапе еще непонятно, а что же такое физически эти нули и единицы, над которыми выполняют операции. Для этого опустимся еще ниже, на уровень микроэлектроники.

Дело в том, что ячейки памяти и регистры на микроэлектронном уровне состоят из транзисторов. Транзистор – это, в первую очередь, полупроводниковый прибор с одним или несколькими p-n переходами разного типа проводимости и, обычно, с 3 выводами. Транзисторы фактически имеют два состояния: включенное и выключенное (открытое и закрытое). Кремний одного типа проводимости находится с двух крайних сторон транзистора (обычно n типа), разделенный материалом другого типа проводимости посредине (p типа), препятствующий свободному прохождению тока (иначе бы транзистор всегда был открыт) – получается структура n-p-n. Полупроводниковый кремниевый материал способен проводить ток только при определённых обстоятельствах: приложении тока или напряжения к его p-nпереходу. Увы, школьное представление о транзисторах не совсем верно. В школе под транзисторами подразумевают биполярные транзисторы. Такие транзисторы управляются изменением тока. В современной компьютерной технике принято использовать полевые транзисторы, управляемые изменением напряжения канала между полупроводниковыми структурами разного типа проводимости (электрическим полем).

Транзисторы: 0 и 1

В современном компьютере используются полевые транзисторы MOSFET, имеющие самое низкое энергопотребление. Это транзисторы хороши тем, что в выключенном состоянии почти не потребляют энергии. Во включенном состоянии канал открыт, ток проходит через транзистор, в выключенном состоянии наоборот. При включении такого транзистора в схему, можно на его выходе получить выходное напряжение определённого уровня – высокого (обычно от 1,7 Вольта и выше), которое соответствует логической единице и низкого (около 0 Вольт), которое соответствует 0. В итоге получается, что, подавая определённое напряжение на вход транзистора и управляя состоянием транзистора с помощью управляющего напряжения на затворе (закрытое или открытое), на выходе можно получать искомые нами 0 и 1.

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

Черным обозначены металлизированные выводы, желтое – изолятор (диоксид кремния SiO2), серым – кремний p-типа, белым – кремниевая подложка N-типа.

Для наглядности, что получается в итоге, вот пара фотографий с микроскопа:

Так выглядит чип советской микросхемы К155ЛА1, включающей 10 транзисторов, 8 диодов и 10 резисторов на одном кристалле. Вы видите только распайку выводов от них. Если вам интересно подробнее узнать, что находится внутри микросхем, почитайте эту статью.

Теперь вернемся к тому, что мы уже знаем: процессор состоит преимущественно из регистров, память – из ячеек памяти. Как происходит их работа?

Оперативная память

Простейшая ячейка оперативной памяти DRAM (Dynamic Random Access Memory) состоит из одного MOSFET транзистора с каналом n-типа и одного конденсатора. Конденсатор (C 4 на картинке) хранит заряд определенное время, который стекает на землю, поэтому состояние заряда надо обновлять с определенной задержкой. Для этого как раз и служит транзистор, который служит в качестве ворот, открываемых для чтения и записи состояния сигнала с определенным интервалом (несколько наносекунд). Каждая такая ячейка находится на пересечении двух линий: источника напряжения – строки и столбца. Управляющий сигнал подается «по строке» (word line), открывая транзистор для чтения и записи с равным интервалом, обратно пропорциональном тактовой частоте памяти. Битовая линия «столбца» (bitline) служит для определения выходного напряжения транзистора в случае чтения ячейки, и для подачи входного 1 или 0 в режиме записи. Как вы понимаете, каждая ячейка может хранить 1 бит.

Ячейки памяти расположены в виде матрицы, а это значит, что каждая из них имеет определенный номер строки и номер столбца. Именно эти данные можно использовать для адресации памяти: обращении к той или иной ячейке по её адресу.

Процессор

В любом процессоре много различных блоков, но большую часть занимают регистры. Регистры предназначены для хранения текущих команд и данных, которые используются устройствами. Регистры состоят из триггеров, которые, в свою очередь, состоят из транзисторов. Триггер имеет два или более устойчивых состояния, и ему, в отличие от ячейки памяти, не требуется «перезаряд» с определенным интервалом. Он хранит текущее состояние, пока не придет входной управляющий сигнал, который его изменит. Поэтому триггеры еще называются «защелками». Каждый триггер реализован на 2, 4 или более транзисторах.

Например, для снятия 32 бит информации с параллельной шины, берут 32 D-триггера и объединяют их входы синхронизации для управления записью информации в защёлку, а 32 D входа подсоединяют к шине.

Последовательный синхронный регистр на D-триггерах

Знакомая фраза «32-битный»? Да, речь именно о том, что в 32-битных системах операции осуществляются сразу за один такт с группами по 32 бита данных: запись, чтение, хранение. Соответственно, в 32-битном процессоре для записи в 32-битный регистр требуется 32 D-триггера и минимум 128 транзисторов. Как вы можете догадаться, в современных 64-разрядных процессорах используются 64-битные регистры.

Существует множество различных регистров в процессоре. По назначению регистры различаются на:

  • аккумулятор — используется для хранения промежуточных результатов арифметических и логических операций и инструкций ввода-вывода;
  • флаговые — хранят признаки результатов арифметических и логических операций;
  • общего назначения — хранят операнды арифметических и логических выражений, индексы и адреса;
  • индексные — хранят индексы исходных и целевых элементов массива;
  • указательные — хранят указатели на специальные области памяти (указатель текущей операции, указатель базы, указатель стека);
  • сегментные — хранят адреса и селекторы сегментов памяти. В защищенном режиме работы процессора сегментные регистры содержат селектор сегмента памяти, выделенного ОС;
  • управляющие — хранят информацию, управляющую состоянием процессора, а также адреса системных таблиц.

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

Как работает процессор?

Ответ на этот вопрос отчасти объяснит, как работает весь компьютер. Однако, этому вопросу посвящены целые многотомники. Мы попытаемся ответить на него в нескольких абзацах.

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

Последовательность машинных инструкций, которые необходимо выполнить образует понятие машинный код. Для его исполнения в процессоре предусмотрен специальный блок – Устройство Управления (Control Unit, CU). На вход этого блока поступает очередная инструкция программы и сохраняется в регистре команд. На выходе УУ выдает последовательность импульсов управления на шину данных и шину адреса. Мы помним, что отдельный регистр IP указывает, какая инструкция из последовательности сейчас должна выполняться, поэтому процессор всегда «знает», что он делает сейчас и что нужно делать следующим шагом.

Современные процессоры способны выполнять несколько машинных инструкций за один такт (синхросигнал). Это значит, что в процессоре с частотой 1 ГГц одновременно выполняется несколько миллиардов операций в секунду.

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

Разновидностью регистров являются порты, через которые происходит ввод-вывод информации. Скажем, адрес порта клавиатуры – 60h. Порт 60h при чтении содержит скан-код последней нажатой клавиши. А к часам реального времени на материнской плате можно обратиться через порт 70h, чтобы получить текущее время. Точно также процессор может общаться и с любыми другими периферийными устройствами – жестким диском, сетевой картой и т.п. Само общение происходит через системную шину, часть которой отведено под передачу данный, а другая часть – под передачу адреса.

Создание машинного кода

Осталось выяснить, откуда берутся эти последовательности инструкций. Потенциально, можно писать программы в машинном коде – для этого существует язык Ассемблера, который программисту позволяет работать напрямую с железом, однако сейчас это делается редко в силу громоздкости кода и трудоёмкости ручного управления ресурсами процессора.

Для примера приведу пример программы на Ассемблере, которая выводит на экран текущие координаты указателя мышки во время исполнения кода (150 строк кода). Эта программа запущена в эмуляторе, что позволяет видеть, что происходит с содержимым регистров и памяти во время исполнения, а также, что будет на экране.

Мы видим, что адрес следующей исполняемой команды (в шестнадцатеричном коде) – 0700:0449h. Регистры CS, DS и ES указывают на адрес 700h в памяти, где располагается запущенная программа. Регистр IP указывает на адрес 449h, который говорит о том, какую следующую инструкцию исполнять в сегменте памяти, начинающемся с адреса 700h (смещение от начала этого сегмента).

Вам ничего не понятно на картинке? Так и должно быть 🙂

Пример исполнения ассемблерного кода в x86 процессоре

На самом деле, не все программы такие сложные. Например, вот код для получения введенного символа с клавиатуры и вывода его на экран занимает всего несколько строк:

Пример исполнения ассемблерного кода в x86 процессоре

Мы видим, что во время исполнения ассемблерной команды int 16h она декодируется Устройством Управления (УУ) процессора в набор похожих простых машинных операций по сложению содержимого нескольких регистров (команда ADD).

Осталось только закольцевать исполнение этого кода с определенным интервалом, и код будет постоянно считывать содержимое буфера клавиатуры, перемещать его в регистры процессора, а затем из регистров процессора на экран!

Мы уже увидели простую магию: как то, что вводится с клавиатуры попадает в регистры процессора и затем аналогичным образом выводится на экран. Но как же действия пользователя интерпретируются в машинный код?

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

Результат компиляции — исполнимый модуль – обычно это файл с расширением EXE или COM, который сохранен в бинарной (двоичной форме). Такой файл при запуске может использовать различные ресурсы операционной системы, например, её встроенные библиотеки, которые потребуются во время исполнения кода. Когда вы запускаете исполняемую программу, она помешает себя в оперативную память, а также подгружает в память необходимые ей библиотеки также в бинарном виде. Пример таких библиотек в Windows — .NET Framework и Visual C++ Library. Библиотеки (файлы с расширением DLL) сами по себе тоже содержат куски кода готовых функций и тоже являются бинарными файлами. С этого момента процессор может исполнять такой код.

Windows, драйверы и порочный круг

ОС Windows – это не то, что вы о ней знали. Это в первую очередь, ядро.

Ядро и подсистемы Windows

Современная Windows работает в защищенном режиме (режиме ядра), которое обеспечивает следующие важнейшие процессы:

  • защита памяти и ввода/вывода
  • многозадачность с механизмом переключения задач
  • организация памяти путем деления её на защищенные сегменты или страницы

Многозадачность достигается на самом деле быстрым переключением между задачами. То есть процессор не одновременно исполняет инструкции, относящиеся к разным запущенным программам, а просто часто переключается между ними. При каждом переключении происходит смена контекста, то есть состояние всех регистров процессора сменяется на то, которое относится к новой задаче. Чем больше запущенно процессов – тем больше конкуренция за ресурсы и тем чаще переключение по кругу. Вот простое объяснение, почему при запуске большого количества программ, компьютер начинает тормозить 🙂

Деление памяти на страницы и сегменты позволяет ограничить доступ программ в «чужие» области памяти, тем самым защитить от повреждения кода.

Помимо перечисленного ядро обеспечивает взаимодействие всех остальных подсистем операционной системы. Прикладная программа работает с интерфейсом программирования API, предоставляемым ей нужной подсистемой. Защищенная исполняемая часть состоит из комплекта подсистем, микроядра и HAL. Набор подсистем и микроядро находятся в файле ntoskrnl.exe. HAL же находится (как можно интуитивно догадаться) в файле hal.dll. На рисунке представлена архитектура Windows NT:

Архитектура и ядро Windows NT

Обо всех компонентах рассказать в рамках этой статьи возможности нет. Выделю главные исполнительные компоненты.

Менеджер виртуальной памяти управляет выделением памяти в куче для кода режима ядра, выделением памяти для пользовательских приложений, виртуализацией запросов (создание иллюзии наличия свободной памяти путем выделения страниц на жестком диске — paging). Одним словом, управляет памятью от имени операционной системы.

Очень важная подсистема Windows — Win32 API. Она представляет собой системные библиотеки и отвечает за графический интерфейс пользователя (библиотеки GDI - Graphical Device Interface), за обеспечение работоспособности Win32 API и за консольный ввод/вывод. Каждой реализуемой задаче соответствуют и свои функции: функции, отвечающие за графический интерфейс, за консольный ввод/вывод, функции управления потоками, файлами и т. д. За графический интерфейс отвечает библиотека GDI32.DLL, которая в режиме пользователя реализована как набор специальных системных вызовов, ведущих в win32k.sysИ еще одна важная вещь — это NTDLL.DLL. Этот файл содержит особую систему, поддерживающую подключаемые DLL-библиотеки.

Зачем я рассказываю все эти детали? Вы поймете чуть позже.

Драйверы

Драйверы добавляют «понимания» операционной системе как общаться с тем или иным железом. Операционная система управляет некоторым «виртуальным устройством», которое понимает стандартный набор команд. Драйвер переводит эти команды в команды, которые понимает непосредственно устройство. При установке драйвера, в системные папки Windows добавляются DLL-библиотеки и файлы с расширением SYS, которые содержат системные функции (куски кода) для работы с соответствующим устройством.

Существуют драйверы для любых устройств: даже процессора и чипсета материнской платы, не говоря уже о всех периферийных устройствах. Драйвер процессора Intel, например, называется intelppm.sys.

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

Приложение пользователя, работающее в Windows, может взаимодействовать с устройством через режим ядра, на котором работают важные системные службы, посредством обращения к функциям интерфейса прикладного программирования (функции Win API), которые включены в некоторые библиотеки динамической компоновки, например, в kernel32.dll. Функции Win API позволяют обратиться из программы, работающей в режиме пользователя, к системным функциям, работающим в режиме ядра, но программы пользователя НЕ используют подсистемы ОС напрямую!

Функции Win API обращаются к Диспетчеру ввода-вывода (I/O Manager), который является важным звеном в цепи взаимодействия приложения и устройства ввода вывода, осуществляя взаимодействие программы пользователя, с одной стороны, и устройства посредством его драйвера, с другой.

В итоге цепочка получается такая: ПО → WinAPI запросы с использованием системных библиотек → I/OManager, обращение к драйверам → формирование исполняемого машинного кода → размещение его в памяти → Ядро: сообщение процессору о необходимости переключиться на новую задачу с новым контекстом в памяти → процессор переключается на новый контекст, исполняет код, формирует необходимые сигналы на линиях системной шины → вывод/запись необходимой информации в регистры устройства.

Прерывания

Прерывание (interrupt) извещает процессор о наступлении высокоприоритетного события, требующего прерывания текущего кода, выполняемого процессором. Процессор отвечает приостановкой своей текущей активности, сохраняя свое состояние, и выполняя функцию, называемую обработчиком прерывания (или программой обработки прерывания), который реагирует на событие и обслуживает его, после чего возвращает управление в прерванный код.

Каждому прерыванию соответствует свой обработчик – небольшой машинный код, который запускается при наступлении прерывания. Чаще всего речь идет о внешних аппаратных прерываниях от периферийных устройств: сигнал от таймера, сетевой карты или дискового накопителя, нажатие клавиш клавиатуры, движение мыши. Факт возникновения в системе такого прерывания воспринимается как запрос на прерывание (Interrupt request, IRQ) — устройства сообщают, что они требуют внимания со стороны ОС, однако не всегда могут это внимание получить.

Линии IRQ идут напрямую от устройств по шинам к процессору и закреплены за устройствами.

Существуют также программные прерывания, которые выполняются в коде с помощью инструкции Int. Обработку таких прерываний также выполняет процессор, но сами обработчики для этих прерываний хранятся в прошивке BIOS компьютера.

Помните у нас в примере с клавиатурой выше были строки Int 10h и Int 16h? Так вот, да, мы с вами вызывали прерывания работы процессора из кода текущей программы и заставляли его выполнять микропрограмму обработчика прерывания и получали в результате код нажатой клавиши клавиатуры в нужном регистре, а потом его же с помощью другой подпрограммы выводили на экран. Да, мы повелевали работой процессора!

Время собирать камни

Ну что ж, пора подводить итоги. Попробуем это сделать на примере. Откроем обычное окно редактора Блокнот и введем туда текст с клавиатуры. Как же это все произведёт в сумме (конечно очень обобщённо)?

  1. Каждое движение мышкой или нажатие клавиши на клавиатуре будет восприниматься как прерывание, которое будет моментально обработано процессором, и он вернется к другим задачам;
  2. Факт выбора вами notepad.exe (который является уже бинарным откомпилированным кодом) и его запуска приведет к системному вызову через WinAPI, который обратится к библиотекам, описывающим код для создания окна на экране компьютера (рамка, менюшки, кнопочки и т.д.).
  3. Этот бинарный код будет размещен в оперативной памяти, а указатель регистра команд будет указывать на адрес в памяти, где данный сегмент начинается. Процессор будет исполнять код, при этом регистр Указателя команд будет с каждой командой смещаться на следующую ячейку в памяти, где хранится код;
  4. После исполнения кода процессор сформирует необходимые управляющие сигналы (высокие и низкие уровни напряжения) и двоичные данные (последовательность 0 и 1, записываемых через шину данных в регистры) для изменения содержимого экрана, которые выставит на шину. Видеоадаптер разместит эти данные в своей видеопамяти и исполнит код (или его исполнит сам процессор, если видеокарта встроенная), который отобразит на экране окно Блокнота;
  5. По мере ввода вами текста будут вызываться программные прерывания клавиатуры, которые будут незамедлительно обрабатываться процессором в виде чтения буфера клавиатуры и предоставления этой информации подсистемам Windows.
  6. Как только Windows получит эти данные, будет формироваться новый код для процессора, чтобы вывести эту информацию на экран по той же схеме, что и в шаге 4.

Весь этот процесс зациклен и представляет 300 тысяч кругов ада, при этом процессор успевает несколько миллионов раз переключиться на другие задачи и исполнить относящийся к ним код.

Обращаю ваше внимание, что драйвер видеокарты при этом не задействован, потому что формирование кода 2D изображения происходит стандартными средствами библиотек Windows. Драйвер видеокарты примет участие в схеме, если пользовательским приложением будет Adobe Photoshop или другое графическое приложение, при этом формирование картинки экрана будет состоять в вычислении цвета каждого пикселя экрана средствами видеодрайвера и формирование кода, который процессор обработает и сформирует команды по обновлению содержимого видеопамяти (в случае дискретной видеокарты этот код формирует не центральный процессор, а графический процессор видеокарты).

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

P.S. Статья посвящена моему другу Руслану, вдохновившему меня на поднятие этого сложного вопроса и написание этой статьи.

Loading

Насколько пост был для вас полезен?

Нажмите на звезду, чтобы оценить мои труды!

Средний рейтинг: 5 / 5. Количество голосов: 1

Пока голосов нет. Проголосуй первым!

Мне жаль, что пост вам не помог 🙁

Позвольте мне исправиться.

Поделитесь, что можно улучшить?

Добавить комментарий