Одним из многочисленных недостатков Windows является огромный зоопарк форматов времени, с которым поневоле приходится иметь дело прикладным программистам. Как результат, обычная на первый взгляд задача превращается в нетривиальную, с поиском единственной из многочисленных API. В данной статье рассматриваются 7 известных мне типов времени, хотя на полноту изложения материал никак не претендует.
Win32API возвращают время в одном из нескольких форматов. Когда нужно, мы можем использовать специальные функции для преобразования из одного формата в другой, например для сравнения, или отображения на устройствах вывода (окно, консоль, принтер). В следующей таблице обобщены основные из них:
1. Системное время – это текущая дата\время суток по Гринвичу UTC. Его возвращает функция GetSystemTime() в структуру SYSTEMTIME, которая содержит по 2 байта в удобном для вывода формате: год, месяц, день недели, день, час, минута, секунда и миллисекунда. Системное время можно установить вызовом SetSystemTime(). Несколько вариантов обозначения одного и того-же системного времени создаёт путаницу, а потому разложим всё по полочкам..
На сегодняшний день, мировое время регулируется выпущенным в 1972 году стандартом UTC. Это время синхронизируется с движением электронов в атоме Цезия, и является точным вплоть до тясячной доли секунд (миллисек=1\1000). Когда такая точность не требуется, альтернативой UTC выступает время по Гринвичу GMT, которое в качестве отправной точки использует уже период вращения Земли за 24 часа. Стоит отметить, что UTC играет роль эталонных часов, и к нему не применяются поправки на зимнее\летнее время.
UTC: Universal Time Coordinated – всемирный стандарт времени.
GMT: Greenwich Mean Time – подразумевает часовой пояс на планете Земля.
STD: Standart Time – локальное время в дефолте.
DST: Daylight Saving Time – локальное время, с поправкой на летнее.
2. Локальное время – является датой и временем для текущего часового пояса, которое мы видим в правом\нижнем углу на панели задач Windows. GetLocalTime() под капотом берёт значение у GetSystemTime(), и преобразует его в местное время на основе настроек часового пояса системы. Поправки можно запросить функцией GetTimeZoneInformation() – на входе она ожидает одноимённую структуру TIME_ZONE_INFORMATION. Для установки лок.времени предусмотрена SetLocalTime().
В таблице ниже перечислены основные города мира относительно GMT(0). Поскольку формат придумали в университете Гринвича в Англии, то конечно они выбрали для себя точку отсчёта нуль. Но если учесть, что Земля круглая, а на карте мира часовые пояса разделены вертикальными блоками, то GMT(0) мог быть назначен кому угодно:
C-подобный:
invoke GetSystemTime, out_lpSYSTEMTIME
invoke GetLocalTime, out_lpSYSTEMTIME
struct SYSTEMTIME
wYear dw 0
wMonth dw 0
wDayOfWeek dw 0
wDay dw 0
wHour dw 0
wMinute dw 0
wSecond dw 0
wMilliseconds dw 0
ends
;//---------------------------
invoke GetTimeZoneInformation, out_TIME_ZONE_INFORMATION
struct TIME_ZONE_INFORMATION
Bias dd 0 ;// значение поправки в минутах (UTC = LocalTime + Bias)
StandardName dw 32 dup(0) ;// Unicode-имя текущего часового пояса
StandardDate SYSTEMTIME ;// дата перехода с летнего на зимнее время
StandardBias dd 0 ;// 0
DaylightName dw 32 dup(0) ;// Unicode-имя летнего времени
DaylightDate SYSTEMTIME ;// дата перехода с зимнего на летнее время
DaylightBias dd 0 ;// 0
ends
Пример вызова функций для чтения системного и локального времени может выглядеть так:
C-подобный:
;//----- Прочитаем системное и локальное время ----------------
invoke GetSystemTime,sTime
invoke GetLocalTime, lTime
cinvoke printf,<10,' System time: ',0>
mov rsi,sTime
call PrintDateTime
cinvoke printf,<10,' Local time: ',0>
mov rsi,lTime
call PrintDateTime
;//----- Инфа о часовом поясе ---------------------------------
invoke GetTimeZoneInformation,tzInfo
mov eax,[tzInfo.Bias]
or eax,eax
jns @f
mov byte[tZone+24],'+'
neg eax
@@: mov ecx,60
xor edx,edx
div ecx
push rax
lea rbx,[tzInfo.DaylightName]
invoke CharToOemW,rbx,buff
pop rax
cinvoke printf,tZone,rax,buff
3. Время Windows – это количество миллисекунд, прошедших с момента последнего запуска системы. По сути представляет собой время работы самой ОС Windows, а потому никак не связано с датой\временем UTC. На 32-битных системах, функция GetTickCount() возвращала миллисекунды в регистр
EAX
, и соответственно макс.значение не могло превышать 0xFFFFFFFF= 4.294.967.295
тиков таймера. Тогда простой арифметикой можно вычислить и примерное время работы ОС, после которого счётчик переполнялся:4294967295 /1000 = 4294967 сек /60 = 71582 минут /60 = 1193 часов /24 = ~49 дней.
Но с приходом архитектуры х64, разрядность
EAX
увеличилась вдвое, и функция GetTickCount64() возвращает теперь в RAX
счётчик с астрономическим значением – его хватит на 585 миллионов лет безотказной работы системы:18446744073709551615 /1000 = 18446744073709551 сек /60 = 307445734561825 мин
307445734561825 мин /60 = 5124095576030 часов /24 = 213503982334 дней /365 = 584.942.417 лет.
Функции GetTickCount() считают по тикам системного таймера, разрешение которого составляет ~16 милли\сек.
На выходе получаем общее время работы ОС, включая спящий режим и гибернацию.
4. Время прерывания – аналогично времени Windows, только выхлоп не в миллисекундах, а в 100 наносекундных блоках (1 наносек = 0.000000001 сек). Чтобы получить время в секундах, нужно будет результат разделить на 10 млн. Отсчет начинается с нуля при запуске ОС, и увеличивается при каждом прерывании часов, на длину их тактов. Точная величина 1 такта зависит от текущих настроек в BIOS аппаратного оборудования (см.ACPI-таймер), а потому может различаться на разных системах.
Для чтения тиков прерываний предусмотрены функции QueryInterruptTime() и QueryUnbiasedInterruptTime(). Несмещённая «Unbiased» версия возвращает время, когда система находилась именно в рабочем состоянии, без учёта спящего режима и гибернации. В Win10 подвезли ещё 2 варианта этих функций с суффиксами «Precise», для более точных измерений интервалов: QueryInterruptTimePrecise(), и QueryUnbiasedInterruptTimePrecise(). Отличаются они тем, что если первые читают данные из уже готовой структуры системы KUSER_SHARED_DATA (отображается в пространство юзера), то расширенные версии напрямую из портов физ.оборудования.
C-подобный:
;//----- Время работы Windows по тикам ------------------------
invoke GetTickCount64
push rax
mov rbx,1000
xor rdx,rdx
div rbx ;// в секундах
mov rbx,60
xor rdx,rdx
div rbx ;// в минутах
xor rdx,rdx
div rbx ;// в часах
mov r10,rdx
pop rbx
@@: cinvoke printf,<10,\
10,' TickCount64: %u',\
10,' Windows time: %d hour, %02d minutes',10,0>,rbx,rax,r10
;//----- Время работы Windows по прерываниям ------------------
invoke QueryUnbiasedInterruptTime,intTime
mov rax,[intTime]
mov ebx,10000000
xor edx,edx
div rbx
mov ebx,60
xor edx,edx
div rbx
xor edx,edx
div rbx
xchg rbx,rdx
cinvoke printf,<10,' Interrupt ticks: %I64d',\
10,' Unibased time: %d hour, %02d minutes',0>,[intTime],rax,rbx
5. Время файла – это 64-битное значение, представляющее количество 100 нано\сек интервалов, прошедших с 01.01.1601 года по Григорианскому календарю. Данный тип времени лежит в компетенции файловой системы, которая обновляет его в момент создания, записи и попытки доступа к файлам. NTFS хранит время в формате UTC без поправок на часовой пояс, в то время как FAT32 использует локальное время компьютера. Все функции работы с файлами возвращают его в 8-байтную структуру FILETIME.
Специально предназначенная для этих целей GetFileTime() извлекает время файла по открытому дескриптору сразу в три структуры FILETIME, отдельно для времени создания, записи и доступа (см.аргументы out_xx ниже). Но это не единственная API в своём роде. Например FindFirst\NextFile() в свою структуру WIN32_FIND_DATA возвращает намного больше информации о файле, в том числе и три указанных варианта файлового времени. Если мы хотим вывести его на консоль или в форточку гуя, нужно функцией FileTimeToSystemTime() преобразовать его сначала в системное. В обратную сторону работает GetSystemTimeAsFileTime(), которая на автомате считывает текущее системное время, и переводит его в формат файлового.
C-подобный:
invoke GetFileTime, in_hFile, out_lpCreate, out_lpAccess, out_lpWrite
struct FILETIME
dwLowDateTime dd 0
dwHighDateTime dd 0
ends
;//---------------------------------------
invoke FindFirstFile,<'*.exe',0>, WIN32_FIND_DATA
struct WIN32_FIND_DATA
dwFileAttributes dd 0
ftCreationTime FILETIME
ftLastAccessTime FILETIME
ftLastWriteTime FILETIME
nFileSize dq 0
dwReserved dq 0
cFileName db 256 dup(0)
cAlternateFileName db 14 dup(0)
ends
invoke FileTimeToSystemTime, in_lpFileTime, out_lpSystemTime
invoke GetSystemTimeAsFileTime, out_lpFILETIME
6. Дата и время MS-DOS – является упакованным 32-битным значением (4 байта). В старших 16-бит кодируется дата, а в младших – время. Как и предыдущем случае, контроль над этих типом времени лежит на плечах файловой системы – FAT обновляет его в момент, когда файл создаётся или модифицируется. Приложения MS-DOS извлекают эту дату и время с помощью функции
AH=2Ah
прерывания int-21h
. Что касается защищённого режима Win, то при работе с файлами DOS, функция GetFileTime() автоматически преобразует дату и время MS-DOS в формат времени UTC. Более того, в клинических случаях можно позвать на помощь и функцию DosDateTimeToFileTime(), которая сбросит время эпохи неолита в структуру FILETIME. Для обратного преобразования имеется так-же и FileTimeToDosDateTime(). В общем здесь есть из чего выбирать.7. Дата и время UNIX – в документации и различного рода утилитах по сбору информации, это время известно как TimeDateStamp. Очень интересный персонаж, а потому уделим ему чуть больше внимания, не в пример программерам из Microsoft, которые решили не заворачить проблему в Win32API. Как результат, все манипуляции с данным типом времени приходится реализовать вручную.
Дело в том, что формат используется в заголовке РЕ-файлов, где для него выделено специальное поле. Как видно из скрина ниже, время создания файла кодируется 32-битным значением DWORD, а поскольку точкой отчёта является 01.01.1970 года (т.н. эпоха Unix), то переполнение поля получим уже в начале 2038 года.
Время внутри заголовка РЕ-файла никак не связано с файловым временем NTFS – его прописывает компилятор при сборке EXE\DLL\SYS. Например, исполняемый файл может быть реально создан в 2000 году, а скопирован поверх файловой системы диска намного позже в 2023-м. Так-вот функция GetFileTime() вернёт дату\время именно как 2023 год.
Более того, отметку о времени создания компилятор прописывает не только в заголовок PE-файла. Отдельное поле TimeDataStamp имеется ещё и в заголовках буквально всех остальные секций экзешника, например: экспорта, в импортах IAT\Delay\Bound, ресурсах, отладки, безопасности, и прочих. Такой зоопарк позволяет находить заражённые файлы на диске, поскольку вышедшая из под пера студентов малварь не обращает внимания на многочисленные поля с отметкой времени внутри бинаря. В идеале, в заголовках всех секций время должно совпадать, в противном случае исполняемый файл должен вызывать подозрение.
Выше упоминалось, что в штатной поставке Win нет библиотечных функции для преобразования Unix-времени в системное UTC. Однако имеется специальная формула, по которой можно вручную получить желаемый результат в структуру FILETIME. Далее остаётся вызвать FileTimeToSystemTime(), и выводить готовое время на консоль\окошки. Сама формула имеет такой вид:
FileTime = (UnixTime * 10000000) + 116444736000000000
8. Послесловие
Время играет огромную роль в системе, начиная от примитивной организации задержек, и заканчивая производительности ОС в целом. При желании можно замедлить или ускорить ход часов, что повлечёт за собой глобальные последствия. Поэтому нужно аккуратней относиться к функциями API с префиксами Set_xx(). Кстати многие сетевые адаптеры NIC могут генерить отметки TimeDataStamp не отходя от кассы прямо в своём оборудовании, читая тики аппаратных часов сетевого контролёра (не обращаясь к системным часам на материнской плате). NIC использует такие штампы например для вычисления времени, на которое пакет застрял в сетевом стеке перед его отправкой\получением.
В скрепку положил исходник небольшой программы с вызовом всех перечисленных выше Win32API, и уже скомпилированный пример для тестов. До скорого, пока!