2 ноября 2010 г.

Введение в 64 бита на Windows

64-битные (или, что тоже самое: 64-разрядные) числа, адреса памяти или другие структуры данных - это данные, размер которых не более 64 бит. Соответственно, 64-разрядный процессор (CPU) - это процессор, который основан на/оперирует с данными (регистры, адреса) этого размера, позволяя производить прямые действия с числами этого размера (однако внешние шины данных и адресации могут иметь и иную размерность). 64-битные CPU существовали в суперкомпьютерах ещё в 70-х годах прошлого века. Но в сегменте персональных компьютеров (который до этого был 32-разрядным) они были представлены только в 2003-м году с появлением x86-64 (AMD64).

Оглавление


Что такое 64-разрядный процессор?

В большинстве процессоров для адресации данных в памяти могут использоваться только целочисленные (Integer) и/или специальные адресные регистры. А другие регистры для этого использовать нельзя. Поэтому размер регистров, используемых для адресации, обычно определяет размер памяти, которую можно адресовать напрямую - даже если в процессоре есть регистры большего размера (например, вещественный сопроцессор). Большинство процессоров спроектировано так, что любой один целочисленный регистр может хранить адрес (место) любых данных в виртуальной памяти (т.е. любой целочисленный регистр - это адресный регистр). Поэтому суммарное количество возможных адресов в виртуальной памяти (суммарный размер данных, которые компьютер может хранить в своём рабочем пространстве) определяется размером этих регистров. 32-битный адресный регистр означает что может быть адресовано 232 адресов или 4 Гб памяти. Когда зарождалась эта архитектура, 4 Гб памяти было намного больше типичных в то время размеров устанавливаемой оперативной памяти (4 Мб), поэтому считалось, что этого более чем достаточно для адресации. Ещё одной причиной, почему 4.29 миллиардов адресов рассматривались как подходящий размер для работы: 4.29 миллиардов чисел было достаточно, чтобы присвоить уникальный номер каждой записи в базах данных тех времён.

Большинство современных 64-разрядных процессоров имеют искусственное ограничение на размер памяти, который они могут адресовать - значительно меньший, чем вы можете ожидать от 64 битов. Например, архитектура AMD64 сейчас имеет ограничение в 52 бита на физическую память и поддерживает только 48-ми битное виртуальное адресное пространство. Это 4 Пб (петабайта) и 256 Тб (терабайт) соответственно. Это позволяет иметь достаточно места в обозримом будущем, не платя полную цену за обработку 64-битных адресов полностью.

Аналогичным образом, некоторые 64-разрядные операционные системы Windows имеют искусственные ограничения на размеры памяти, существенно ниже теоретически возможных. Это связано с тем, что разработчики ОС не хотят утверждать, что их продукт сможет работать на конфигурации, которую они не тестировали - поэтому максимальный размер памяти равен самому большому размеру памяти, который удалось протестировать к моменту выпуска ОС. Однако, реальное ограничение может быть ещё ниже из-за редакции ОС. К примеру, Windows 7 Starter поддерживает только 2 Гб оперативной памяти (да, это ограничение 64-разрядной версии). Вот табличка с параметрами систем. Я также рекомендую почитать этот цикл статей (по ссылке - последняя статья из серии, просто там в начале указано полное оглавление).

Помимо увеличенной возможности адресации 64-разрядные процессоры предлагают новые вычислительные возможности в виде новых регистров и команд. Также накладываются дополнительные ограничения и соглашения. К примеру, рассматривая x86-64: математический сопроцессор считается устаревшим. Его использование не поощряется, хотя и технически возможно. Вместо сопроцессора можно использовать 64-разрядные регистры или SSE. Имеются более строгие требования к выравниванию данных. Особенно на стеке. Модель вызова - только одна. Ключевые слова типа stdcall, register и т.п. игнорируются. По этим трём причинам (сопроцессор, выравнивание + передача вещественных чисел в SSE-регистрах), Extended (10 байт, родной для мат-сопроцессора) в мире x64 становится очень уж неудобным. Возможно, он будет как-то изменён в грядущей Delphi x64 (либо приравняют к Double или Single, как это делает Free Pascal, либо пометят как deprecated, либо оставят как есть, но работа с ним может быть не оптимальной). x86-64 даёт вам 16 64-разрядных регистров и 16 128-ми разрядных XMM-регистров (для вещественных чисел). Для сравнения: у x86-32 есть только 8 32-разрядных регистров (если я правильно сосчитал), остальное - это сопроцессор (8 80-битных регистра) и расширения. Т.е. x86-64 практически удваивает набор регистров, разве что регистры для вещественных чисел имеют уменьшенную до Double размерность. Напомню, сам сопроцессор никуда не уходит, но его использование не поощряется. Сопроцессор также не участвует в единственной для x64 модели вызова.

32 бит vs 64 бит

Переход от 32-битной к 64-битной архитектуре является фундаментальным изменением, потому что большинство операционных систем должны быть специально изменены для использования преимуществ новой архитектуры. Стороннее программное обеспечение также должно быть изменено, чтобы воспользоваться новыми возможностями; уже написанные программы поддерживаются либо режимом обратной совместимости в железе (когда один и тот же процессор может выполнять и 64-разрядные и 32-разрядные команды), либо реализацией 32-разрядного ядра процессора рядом с 64-разрядным, либо слоем программной эмуляции. В Windows для x86/x86-64 имеет место первый вариант. При этом самой большой проблемой являются драйвера. Если обычные приложения могут спокойно работать, используя подсистему WOW64, то драйвер обязан работать в родном для системе режиме. Т.е. 32-разрядный драйвер не сможет работать в 64-разрядной системе.

Когда нужно ставить 32-разрядную или 64-разрядную ОС? Простой ответ может выглядеть так: если у вас 4 Гб оперативной памяти и более - то ставить надо 64-разрядную. В противном случае - 32-разрядную. Подробный ответ: всё не совсем так просто. Во-первых, в 32-разрядной системе вам не удастся воспользоваться всеми 4 Гб памяти - реальная цифра будет около 3 Гб (плюс-минус). Происходит это по той причине, что часть пространства резервируется под различные нужды. С другой стороны, на новых процессорах есть Physical Address Extension (PAE), которое даёт вам поддержку 64 Гб памяти - даже в 32-разрядном режиме. Но воспользоваться им вы сможете только на серверных Windows при условии, что все установленные драйвера поддерживают этот режим. Более подробно про PAE написано в этом цикле статей, а также в уже упоминавшемся выше цикле статей от Марка Руссиновича. Кроме того, нужно учитывать и обратную совместимость. Если у вас в машине есть очень старая железка, под которую нет 64-разрядного драйвера - использовать её вы не сможете: придётся ставить 32-разрядную ОС. К счастью, это скорее исключение, чем правило.

Верите вы или нет, но большей части разработчиков не нужна 64-разрядность. Зачем? Их 32-разрядные приложения и так отлично работают. В конце концов, обратная совместимость - конёк Microsoft. Давайте посмотрим, кому реально нужен x64:
  • Логика программы требует ворочать огромными объёмами данных в памяти.
  • Совместимость кода:
    • Кто пишет плагины/расширения для Проводника и других программ.
    • Хуки.
    • Кто работает в команде и пишет DLL, которую грузит x64 exe, писанный в VS.
  • И... всё.
Вложенные три пункта - это ровно одна и та же причина: 64-разрядный процесс не может загрузить 32-разрядную DLL и наоборот. Поэтому, реально причины всего две: вам нужны бонусы огромного адресного пространства или вам нужна интеграция с другим 64-разрядным компонентом. Всё.

Казалось бы, совместимость с 64-разрядным кодом - некая "вещь в себе", но когда к тебе подходит коллега (пишущий на MS VS) и говорит: "Слушай, что за дела? У заказчика все машины 64-битные, весь наш софт 64-битный - но почему ты делаешь 32-разрядные DLL?" и вам приходится отвечать: "Ну, Delphi не умеет делать 64-разрядные DLL" - надо видеть его глаза.

Вторая большая причина - плагины. В том числе и расширения Проводника.

Для всех остальных переход на x64 - только лишняя головная боль, которая не принесёт им ничего, кроме чувства идеальности (да! теперь наш продукт работает в родном режиме на x64!). Более того, это лишь ухудшит их ситуацию: теперь им нужно поддерживать и распространять две версии продукта, вместо одной (которая, напомню, отлично работает и так).

Главным недостатком 64-битной архитектуры является то, что одни и те же данные (и код) будут занимать больше места в памяти и на диске (из-за увеличения размера указателей, других аналогичных типов и выравнивания). Это увеличивает размер необходимой процессу памяти, частично гася работу кэша. Кроме того, 64-разрядное приложение не будет работать в 32-разрядном окружении, поэтому, как я уже указал выше, вам придётся делать две версии своей программы - что означает удвоенное поле для тестирования и поддержки. Поскольку преимущества 64-разрядного режима (работа с очень большими объёмами данных) выгодны только очень ограниченному количеству приложений (типа серверов БД), то мы можем ожидать очень медленный переход на 64-разрядные системы. Конечно, когда-нибудь в будущем 90% всех домашних компьютеров будут иметь 64-разрядные возможности - и написание 32-разрядных программ станет не выгодно.

Сегодня же, большинство программ компилируются в 32-разрядном режиме, и лишь часть - в 64-разрядном. Причём многие 64-разрядные программы являются лишь копией (перекомпиляцией) 32-разрядных программ, никак не использующими преимущества увеличенного адресного пространства и 64-разрядных указателей. Однако, они могут использовать новые регистры процессора (что потенциально может сделать их более быстрыми, если только это не погасится их разбухшим размером кода). Главное преимущество 64-разрядных программ - доступ к новым регистрам.

Примечание 2017 года: уже сегодня даже самые дешёвые неттопы поддерживают архитектуру AMD64 и не меньше 8 Гб памяти. Иными словами, 32-разрядные машины уже больше не выпускаются. И если вы программируете не на Delphi, то уже нет большого смысла писать 32-разрядный код, чтобы он работал через прослойку эмуляции со всеми её подводными камнями. Если вы программируете на Delphi, то... ну, скажем так, 64-разрядный компилятор - не самая вылизанная часть Delphi.

Просуммируем сказанное здесь в табличке (преимущество/недостаток рассматривается в сравнении с соседней моделью):

32-разрядные приложения или архитектура64-разрядные приложения или архитектура
ПреимуществаНедостаткиПреимуществаНедостатки
Приложения работают в любой системе.Приложения работают только в 64-разрядной системе. Вам придётся делать 32-разрядный вариант, чтобы ваше приложение работало в большинстве современных систем.
Приложения имеют меньший размер и требуют меньше ресурсов (памяти) для работы.Размер и требования приложений слегка увеличены за счёт удвоения размера адресов и выравнивания.
32-разрядная DLL может быть загружена в 32-разрядный процесс.32-разрядная DLL НЕ может быть загружена в 64-разрядный процесс.64-разрядная DLL может быть загружена в 64-разрядный процесс.64-разрядная DLL НЕ может быть загружена в 32-разрядный процесс.
Приложение не получает бонуса с выполнения на 64-разрядной системе.Приложение может воспользоваться новыми регистрами процессора.
Сложно работать с большими объёмами памяти. Вам нужно использовать какой-нибудь механизм частичной загрузки/выгрузки. Например, AWE, проецируемые частями файлы или подходящий алгоритм обработки по частям.У приложения есть доступ к 8 Тб адресного пространства.
Работа с 64-разрядными числами возможна, но по частям.Работа с 64-разрядными числами напрямую.
Родной поддержки для 128-разрядных чисел нет.Возможна работа с 128-разрядными числами, но по частям (не ясно, будет ли такая поддержка в компиляторе Delphi).

Историческая справка
Процесс одной размерности может загрузить только модуль той же размерности. Это применимо к 16-, 32- и 64-разрядным процессам и модулям. Вот почему в 64-разрядной системе вы часто видите два варианта данных: один вариант - для 64-разрядных процессов, другой вариант - для 32-разрядных процессов ("Program Files" vs "Program Files (x86)", "System32" vs "SysWOW64").

Однако, при переходе от Win16 к Win32 всё обстояло несколько иначе. Не было двух вариантов кода, а просто использовались переходники от 16-ти разрядным к 32-разрядным модулям. С помощью процесса, известного как generic thunks, 16-ти битная программа может загрузить 32-битную DLL и вызвать её. Windows 95 и слой эмуляции 16-ти битных Windows в Windows NT сильно зависят от generic thunks, так что им не нужно иметь две версии кода (как это имеет место быть для 32- и 64-разрядного кода). Вместо этого, 16-ти битная версия просто использует 32-битную версию через "переходник".

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

WOW (также известный как WOW32) предоставляет специальные функции (в обе стороны), которыми может воспользоваться переходник. Всё это приводит к видимости, что 16-ти разрядный код загружается и используется 32-разрядным. При переходе с 32 на 64 такого механизма не предусмотрено.

64-битные модели данных

Каждое приложение и каждая операционная система имеют абстрактную модель данных. Многие приложения не говорят явно об этой модели, но модель данных управляет тем, как написан код приложения. В 32-разрядной программной модели на Windows (известной как модель ILP32) Integer, LongInt и Pointer были размером в 32 бита. Большинство разработчиков использовало эту модель даже не осознавая этого. За всю историю Win32 это допущение было корректным (хотя и не безопасным).
Историческая справка
Классическая модель данных языка Pascal при переходе от 16- к 32-разрядной архитектуре выглядела так: в языке есть фундаментальные типы данных: ShortInt, SmallInt, LongInt, Int64 (и их беззнаковые эквиваленты), которые всегда имеют фиксированную размерность (8/16/32/64 бита соответственно) и не зависят от целевой платформы. И есть обобщённый тип данных: Integer (и его беззнаковый эквивалент Cardinal), который равен родному для целевой платформы типу. Т.е. Integer = SmallInt на Win16 и Integer = LongInt на Win32. Предполагалось, что Integer будет равен Int64 на Win64.
Но при переходе на Win64 все эти допущения оказываются не рабочими. Посмотрите на эту табличку существующих на сегодня моделей данных 64-разрядной архитектуры:

64-битные модели данных
Модель
данных
SmallInt/
Word
LongInt/
LongWord
Integer/
Cardinal
Int64/
UInt64
Pointer Пример операционной системы с этой моделью
LLP64 16 32 32 64 64 Microsoft Windows (X64/IA-64)
LP64 16 64 32 64 64 Большинство Unix и Unix-like систем, типа Solaris, Linux, and macOS и, конечно же, Android и iOS
ILP64 16 64 64 64 64 HAL Computer Systems порт Solaris на SPARC64
SILP64 64 64 64 64 64 Unicos

При этом в Delphi уже сейчас введены два новых типа: NativeInt и NativeUInt. В 32-разрядном коде эти два типа эквивалентны Integer и Cardinal соответственно. Справка говорит нам, что эти два типа всегда будут равны по размеру в байтах типу Pointer.

"Почему Integer остался 32-разрядным?"

Многие задают вопрос: почему в Win64 Integer остался 32-разрядным? Зачем нужно было вводить новый тип NativeInt?

Основная ошибка в рассуждениях - вы привыкли к одному, довольно странному процессору x86-32. Где, во-первых, целочисленные регистры общего назначения являются одновременно и адресными (это далеко не так у многих процессоров). Во-вторых (и следовательно), размер целочисленных регистров равен размеру адресных. Это опять же не так у других процессоров. У Крей-1, к примеру, регистры общего назначения являются 64-битными (грубо говоря, SizeOf(Integer) = 8), но адресует память он только на 24 бита (грубо говоря, SizeOf(Pointer) = 3).

Теперь понятно, что модель классического Pascal, в которой есть только "просто целочисленный тип" (Integer) и "указатель" (Pointer) - не жизненноспособна как кроссплатформенная модель данных? Вам нужно иметь два обобщённых целочисленных типа. Один - общего назначения (Integer), второй - равный по размеру указателю (NativeInt). Просто так уж совпало, что в x86-16 и x86-32 эти два разных типа были эквивалентны, поэтому в язык Pascal не добавляли отдельный тип вроде NativeInt. Но в x86-64 (и на некоторых других платформах) эти два разных типа уже не равны друг другу. И если раньше мы обходились без NativeInt, то теперь уже никак. Integer и Cardinal - по прежнему процессорно-зависимые. Надо просто понимать, что размер целочисленного типа в процессоре не всегда совпадает с разрядностью указателя.

Окей. Стало понятно, почему появился NativeInt. Но пока открыт вопрос, почему же Integer не стал 64-разрядным? Почему в Windows применяется модель LLP64?

Для этого есть несколько факторов. Возможно, ни один из них не диктует однозначного выбора, но когда вы соединяете всё вместе - выбор становится очевидным.

Во-первых, размер Integer в 32 бита диктует сам процессор (аппаратная часть). Посмотрите на такой код:
Y := X;
Y := Y + 5;
Где X и Y объявлены как Integer. В 16-битном Pascal-е (x86-16) это выглядело так:
Y := X; 
// 89C1           mov cx,ax
Y := Y + 5;
// 83C105         add cx,$05
Здесь идёт работа с двух-байтовыми числами (16 бит).

Когда произошёл переход 16 -> 32, этот же ассемблерный код стал (на x86-32):
Y := X; 
// 89C1           mov ecx,eax
Y := Y + 5;
// 83C105         add ecx,$05
Здесь уже идёт работа с четырёх-байтовыми числами (32 бита). Т.е. один и тот же машинный код (одна и та же последовательность байтов) стал оперировать с целочисленными типами другого размера. Иными словами, родной для процессора целочисленный тип стал другого размера.

Однако, когда произошёл переход 32 -> 64, родной для процессора целочисленный тип не изменился:
Y := X; 
// 89C1           mov ecx,eax
Y := Y + 5;
// 83C105         add ecx,$05
Внезапно, ровно этот же машинный код на x86-64 работает с 32-битными, а вовсе не с 64-битными числами. Чтобы явно указать, что мы работаем с 64-битным расширением, нужно к каждой команде добавлять специальный префикс REX:
Y := X; 
// 4889C1           mov rcx,rax
Y := Y + 5;
// 4883C105         add rcx,$05
С другой стороны, если команда по своей природе оперирует указателем (например, CALL), то префикс REX указывать не нужно. Иными словами, родной для процессора целочисленный тип не изменил своей разрядности в x64, оставшись 32-битным, а указатель поменял разрядность с 32 до 64 бит.

Ещё пример: при расширении 32-разрядного числа до 64-разрядного на x86-64 - старшая часть регистра не обнуляется. Опять же, в отличие от расширения 16-разрядного числа до 32-разрядного на x86-32.

Именно поэтому, для x86-64 самым родным и эффективным является 32-разрядный Integer, а вовсе не 64-разрядный Int64. Почему? Ну потому что в AMD64 64-разрядный режим (long mode) - это простая надстройка над существующим 32-разрядным режимом (protected mode). Это большой плюс к обратной совместимости кода, что и привело к тому, что AMD64 выжила на рынке. Не зря Intel назвала свою реализацию AMD64 "IA-32e" (хотя потом они переименовали её в "EMT64"; "IA-64" же - это был Itanium, это вообще не x86).

Во-вторых, практика показала, что большинству программ даже сегодня не нужна работа с 64-разрядными числами - они прекрасно обойдутся и 32-разрядными. Увеличение размерности до 64 бит было бы пустой тратой места, потому что это не нужно большинству приложений. Да, некоторым приложениям нужно оперировать с данными большого размера, но это необходимо только для специальных узких случаев. Как вы можете видеть, две наиболее популярные модели (LLP64 и LP64) используют именно 32-разрядный Integer. Так что если вы используете Integer как "просто целочисленный тип, удобный для процессора" - вы в порядке.

В-третьих, изначально приложения, которые работают на 64-битной Windows, будут получаться портированием 32-разрядных приложений Windows. Цель состоит в том, что один и тот же исходник, будучи написан аккуратно, должен работать как в 32-х, так и в 64-разрядном окружении. Определение модели данных само по себе не делает эту задачу проще. Но гарантия, что модель данных влияет только на размер указателя, является только первым шагом. Второй шаг - определить набор новых типов данных, которые позволят разработчику автоматически менять размеры данных, связанных с указателями. Это позволит вам иметь целочисленный тип данных, который меняет свой размер вместе со сменой размера указателя от 32 до 64 бит. Базовые типы данных останутся размером 32 бита, поэтому вам не нужно будет производить никаких изменений уже написанного кода, работающим с файлами на диске, передачей по сети или IPC. Это значительно сузит объём работ, необходимых для портирования ваших приложений на 64-битные Windows.

Наконец, есть и причина обратной совместимости: неисчислимое количество кода уже написано в предположении, что размер Integer и LongInt не изменится.

Все эти факторы суммарно и привели к тому к выбору модели LLP64. В модели данных LLP64 только указатели увеличиваются до 64 бит. Все остальные типы остаются без изменений. Иными словами, команда разработчиков Windows сделала переход на 64-разрядную архитектуру максимально безболезненным.

Заметьте, что определение Integer как типа, зависящего от целевой платформы всё ещё не нарушено. Просто в x86-64 он оказался равен LongInt. Ну, бывает.

Заметьте, что программная модель - это выбор на базе компилятора, так что в одной ОС могут сосуществовать разные модели данных. Хотя модель, выбранная самой ОС, обычно доминирует по понятным причинам.

Переход на 64-битную архитектуру (предварительная информация)

Конвертация приложения для 64-битной архитектуры может варьироваться по сложности. В идеальной ситуации вы сможете просто перекомпилировать приложение и оно будет работать. В запущенных случаях вам придётся делать много работы. Основная проблема при переходе - программист посчитал, что некий тип данных совпадёт по размеру (в байтах) с указателем. И это допущение выполнялось на 32-битной машине, но может быть нарушено на 64-битной.

Наиболее вероятно, что будущая 64-разрядная версия Delphi возьмёт модель LLP64 - как взяла её Windows и как следует ей Free Pascal. Поэтому все Integer и LongInt останутся 32-разрядными.

Обновление 2017 года: как мы можем видеть из справки, для Win64 Delphi взяла именно модель LLP64.

Уже сейчас Delphi имеет новые типы данных, которые выглядят намного логичнее старых (ну-ка, не подглядывая в справку: сколько байт занимает SmallInt? А ShortInt?): Int8, Int16, Int32, Int64, NativeInt (и их беззнаковые аналоги: UInt8, UInt16, UInt32, UInt64 и NativeUInt). Типы с цифрами имеют (надеюсь) очевидную размерность, а NativeInt/NativeUInt равен размеру Pointer в байтах. Достаточно очевидно и прозрачно. Integer всё ещё является типом, размерность которого зависит от платформы, но никто не говорил, что его размерность равна размерности указателя. Integer - это просто общий целочисленный тип. В частности поэтому Integer равен 16/32/32 на Win16/Win32/Win64.

Давайте приведём табличку, суммирующую сказанное для целочисленных типов данных:

Целочисленные типы данных в Delphi
ТипЗнаковый?Размер в
Win16
Размер в
Win32
Размер в
Win64
ПсевдонимыЦель/назначение
PointerX163264Указатель
NativeInt+163264Ошибка природы
(знаковый целочисленный тип, совместимый с указателем)
NativeUInt-163264Беззнаковый целочисленный тип, совместимый с указателем)
Int+163232IntegerЗнаковый целочисленный тип общего назначения
UInt-163232CardinalБеззнаковый целочисленный тип общего назначения
Int8+888ShortIntЗнаковый целочисленный тип размером 8 бита
UInt8-888ByteБеззнаковый целочисленный тип размером 8 бит
Int16+161616SmallIntЗнаковый целочисленный тип размером 16 бит
UInt16-161616WordБеззнаковый целочисленный тип размером 16 бит
Int32+323232LongIntЗнаковый целочисленный тип размером 32 бита
UInt32-323232LongWordБеззнаковый целочисленный тип размером 32 бита
Int64+X6464Знаковый целочисленный тип размером 64 бита
UInt64-X6464Беззнаковый целочисленный тип размером 64 бита

Заметьте, что псевдонимы указаны именно для Windows (Win16, Win32, Win64). К примеру, Android и iOS используют модель LP64, где LongInt и LongWord имеют размер 64 бита. Если в Delphi появится поддержка 64-битных Android и/или iOS, то наиболее вероятно, что Delphi также будет использовать модель LP64, т.е. LongInt и LongWord станут восьмибайтовыми.

Примечание 2017 года: как мы можем видеть из справки, Delphi взяла модель LP64 для 64-битной iOS.

Итак, что же со всем этим делать? Ну, вам нужно следовать нескольким простым правилам:
  • Если вам нужна целочисленная переменная для общих целей (счётчик цикла, например) - берите Integer.
  • Если вам нужна целочисленная переменная строго фиксированного размера (например, для записи в файл, отправке по сети или другому приложению через IPC) - берите (U)IntX (предпочтительно) или любой фундаментальный тип - LongInt, SmallInt и т.п. (сильно менее предпочтительно, особенно на Android/iOS, но хотя бы привычнее). Однозначно не используйте для этого обобщённые типы Integer или Cardinal.
  • Если вам нужен тип для "Pointer-like" данных - берите NativeUInt.
  • Никогда не используйте NativeInt.
  • По возможности не используйте ShortInt, Byte, SmallInt, Word, LongInt и LongWord.
  • Вам нужно внимательнее относится к типам данным. Например, тип Integer больше не взаимозаменяем с HWND или THandle в Win64.
Остаётся только согласовать код в местах прикосновения разных типов.
Примечание
В Delphi крайне неудачно выбрано имя нового типа (NativeInt). Во-первых, префикс "native" намекает на то, что этот тип - "родной" для процессора и что его нужно использовать всегда. Что, мягко говоря, далеко от истины. "Родной и удобный" целочисленный тип - это Integer. Во-вторых, в некоторых старых версиях Delphi (я затрудняюсь назвать версии; думаю, что где-то в районе Delphi 7) тип с именем NativeInt уже был. Вот только его размер был... 8 байт - что явно не равно положенным 4 байтам для 32-разрядного компилятора. В-третьих, сама по себе концепция "знаковый целочисленный тип эквивалентный указателю" уже заметно попахивает. Указатель не может быть отрицательным. Иными словами, вместо двух типов NativeInt и NativeUInt в Delphi следовало бы добавить только один тип PtrUInt (чего, увы, не произошло).

Вероятно, позднее Embarcadero опубликует какое-нибудь руководство по переходу на Win64, как она сделала это для Unicode.

Что делать, если вы писали свой код в расчёте на то, что Integer станет равен Int64, а LongInt останется 32-битным? Иными словами, вы думали, что переход на 64 будет совершаться так же, как и переход на 32. А действовать надо ровно так же: вы находите все места использования 64-разрядных чисел и меняете тип с Integer на NativeInt. С другой стороны, вы могли бы сделать простой Search&Replace по исходному коду: Integer -> NativeInt. Да, это будет работать, но надо понимать, что тогда ваша программа будет везде оперировать с 64-разрядными числами (вы ведь использовали Integer как "просто целочисленный тип, удобный для процессора", не так ли?), что, как мы узнали, является не самым эффективным действием на x86-64.

Ну и, напоследок, несколько фактов:
  • 64-разрядное приложение использует формат PE, так что размер exe-файла не может превышать 2 Гб.
  • SizeOf(THandle/HWND/HMODULE/Hxxxx) = SizeOf(Pointer) = 8. Поэтому вот так будет неправильно: Value := Integer(Handle).
  • Свойства вроде Tag (вероятнее всего) станут типа NativeInt.
  • Пожалуй, единственными 32-разрядными приложениями, которые получают какие-то бонусы с их выполнения на 64-разрядной системе, являются приложения, помеченные флагом LARGEADDRESSAWARE. На 32-разрядной системе таким приложениям дадут только (максимум) 3 Гб, а вот на 64-разрядной системе в их распоряжении будут все 4 Гб.
См. также:

27 комментариев:

  1. Зачетненько.
    Развеелись страхи о переходе на 64бита а так же раскрыта тема - А оно мне нужно? =)

    ОтветитьУдалить
  2. Отлично! Спасибо!

    ОтветитьУдалить
  3. Поддержку коллег - очень интересная статья.

    Я буду пользоваться 64 - у меня есть одно серверное приложение, для которого было бы неплохо иметь пару сотен гигабайт оперативной памяти.

    ОтветитьУдалить
  4. Отдельное спасибо за раздел "64-х битные модели данных" и табличку в нём. Очень удобно и наглядно всё собрано.

    ОтветитьУдалить
  5. >Модель вызова - только одна. Ключевые слова >типа stdcall, register и т.п. игнорируются.
    Если немного подумаете, то модель вызова и на x86 была одна :)
    CALL - он CALL и для stdcall и для register.
    Процессор x86 ничего не знает ни про stdcall, ни про register.
    Модель вызова - это абстракция на уровне языка программирования.
    Или я чего-то путаю? :)

    ОтветитьУдалить
  6. >а вот на 64-х разрядной системе в их распоряжении будут все 4 Гб

    Точнее - 3.5 Гб

    ОтветитьУдалить
  7. Насколько я помню stdcall - механизм вызова в виндовом апи, когда аргументы распложены в стеке справа налево.
    Это вроде никакого отношения к разрядности не имеет.

    ОтветитьУдалить
  8. > Точнее - 3.5 Гб

    Неверно. Точный предел - 4026 Мб.

    > модель вызова и на x86 была одна

    Call - это не модель вызова. Это инструкция процессора.

    Соглашение вызова или модель вызова — регламентирует технические особенности вызова подпрограммы (передачи параметров, возврата из подпрограммы и передачи результата вычислений в точку вызова). Как несложно сообразить, делается это в тесном сотрудничестве с процессором. Модель вызова зависит от процессора И от программной части. Модель вызова IA-64 вы при всём желании не перетащите на x86. Поэтому и говорят о модели вызова процессора. Странно, что это приходится разжёвывать.

    Рекомендую ещё этот пост и эту серию.

    > Процессор x86 ничего не знает ни про stdcall, ни про register.

    Угу, а ещё он про стек ничего не знает :)

    ОтветитьУдалить
  9. > Это вроде никакого отношения к разрядности не имеет.

    Модель вызова и архитектура процессора - тесно связанные понятия.

    ОтветитьУдалить
  10. >Модель вызова и архитектура процессора - тесно связанные понятия.
    Ой ли :)
    http://en.wikipedia.org/wiki/X86_calling_conventions

    - The cdecl calling convention is used by many C systems for the x86 architecture.
    - Syscall is the standard calling convention for 32 bit OS/2 API.
    - Optlink is used by the IBM VisualAge compilers
    - pascal
    - register(An alias for Borland fastcall)
    - stdcall
    - fastcall
    - Microsoft fastcall
    - Borland fastcall
    - Watcom register based calling convention
    - TopSpeed / Clarion / JP
    - safecall
    - thiscall
    for x64:
    - Microsoft x64 calling convention
    - AMD64 ABI convention (is followed on Linux and other non-Microsoft operating systems)

    Метод передачи параметров зависит не от разрядности процессора, т.к. у процессора вообще нет такой сущности как параметр. У него есть только регистры, память и стек. И как там будут размещены параметры - не процессору решать.
    Умеете признавать ошибки? :)

    ОтветитьУдалить
  11. > у процессора вообще нет такой сущности как параметр.

    Нету. Я и не спорил. И что это меняет? И, кстати, этого:

    > И как там будут размещены параметры - не процессору решать.

    я не говорил. Я сказал: "Модель вызова и архитектура процессора - тесно связанные понятия". Где тут про "процессору решать"?

    Ну-ка, если соглашение вызова от процессора не зависит - перетащите-ка мне с x86 на IA-64 соглашение register (которое, напомню, использует стек сопроцессора, которого у IA-64 нет, для передачи вещественных чисел). Или перетащите с IA-64 на x86 модель вызова IA-64 (которая, напомню, использует 96 регистров при вызове функции. Причём функция говорит, какие из этих регистров входные, а какие - выходные. Плюс там есть регистровый стек, плюс...).

    Дааа, совсем никак не связанные понятия, полностью независимые, ага.

    ОтветитьУдалить
  12. Даже для x86 приложений лучше не делать Integer(THandle). Т.к. следующим шагом программиста будет присвоение этого значения типу THandle (сам я передавал как параметр в WinAPI). В Win32 всё в порядке, а вот в Win64 грабли - значение THandle бывает больше 2^31. Так что в результате получаем ошибку переполнения значения.

    ОтветитьУдалить
  13. Отличная статья, спасибо. Особенно про разбор типов.

    Что до "поддержки двух версий программ" - это нормальная ситуация. Те, кто не хотят заморочек с разной архитектурой процов, пишут на .NET, и правильно делают.

    Что до выбора ОС "для себя" - я думаю, чем скорее отомрет x86 (32bit), тем будет лучше для всех.
    Он славно послужил нам, но - тем не менее.
    Поэтому "колеблющихся" надо агитировать за 64 :)

    Я, имея на борту домашнего компа ровно 4G памяти + PAE для 32-эмуляции, поставил 64-битную Винду. И не жалею.

    ОтветитьУдалить
  14. Я буду пользоваться 64 - у меня есть одно серверное приложение, для которого было бы неплохо иметь пару сотен гигабайт оперативной памяти.
    Поспешу вас разочаровать. Кроме поддержки процессором, нужна еще поддержка материнкой.
    Даже неплохие серверные платформы имеют куда меньший лимит (к примеру, такая - 144 Gb).

    ОтветитьУдалить
  15. Считаю что 64-бит надуманная проблема и вообще не нужно.
    Судите сами при 32-х разрядной архитектуре уже давно успешно используются винты более 4 гб. И файлы можно создавать более 4 гб. Почему тоже самое нельзя сделать с памятью. Тем более уже существуют устройства наподобие ACARD ANS-9010, где можно тупо подключить данное устройство вставить сколько нужно памяти и работать с данными как с файлом, но при этом файл размещается в памяти. Никакие костыли более просто не нужны.
    Только нужно дать напутсвенного пинка манагерам, чтобы они наконец зашевелили своей толстой задницей и организовали повсеместную продажу данного устройства.

    ОтветитьУдалить
  16. Когда последний раз писали код с File Mapping или AWE? Хочется вернуться во времена сегментированных Win16?

    Нет уж, спасибо. Вот уж действительно, чем скорее x86 вымрет, тем лучше для всех.

    ОтветитьУдалить
  17. Работал с файлами на winAPI строил бинарное дерево в файле (правда на ram-диске). Указываешь адрес в int64 и ходишь по всему файлу куда хочется. Все идет целиком в одном файле. Удобно проектировать, программировать и отлаживать.

    P.S. На форумах пишут что есть проблемы при выделении >2 гб общим массивом даже если система полностью 64-бит (железо, ОС, компилятор). Нужно выделять сегменты памяти в отдельных процессах. В общем 64-бит это тот те же AWE только в профиль.

    ОтветитьУдалить
  18. За девайс Acard ANS-9010 +500 в карму

    ОтветитьУдалить
  19. Уважаемый блогер. Будьте так добры. Прокоментируйте этот пост http://www.ixbt.com/soft/windows-4gb-2.shtml и напишите статью что нужно сделать чтобы выделить приложению >4 гб в 64 битных ОС на практике (желательно с примером). Иначе получается как писал товарищ выше, 64 бит действиетельно не нужно. Также как и нет смысла устанавливать >4гб на комп.

    ОтветитьУдалить
  20. Вы просите меня дать вам решение проблемы, которую вы не озвучили. В Win64 нет никаких проблем с выделением памяти > 4 Гб.

    // Всегда успешно в Win64 - вне зависимости от кол-ва ОЗУ
    // (Разумеется при стандартных условиях на наличие свободного места в адресном пространстве и файле подкачки)
    AllocMem(NativeInt(8) * 1024 * 1024 * 1024 { 8 Гб } );

    Вы путаете ОЗУ (RAM) и (виртуальную) память приложения. Рекомендую ознакомиться.

    ОтветитьУдалить
  21. Неплохой обзор. Вообще, с поддержкой 32-битных приложений на 64-разрядных есть ряд специфики. Например, wow64 хоть и позволяет запускать 32-бита, но не позволяет инъекции кода. Именно по этой причине большинство программ модифицирующих проводник Windows, не запускаются. Подробнее об этом и о других особенностях можно узнать в обзоре "32 битная программа на 64" по адресу http://ida-freewares.ru/support-32-bit-app-in-64-app-windows.ht

    ОтветитьУдалить
  22. >>> wow64 хоть и позволяет запускать 32-бита, но не позволяет инъекции кода

    Вообще-то, никто не запрещает 32-битному коду использовать WriteProcessMemory для внедрения 64-битного кода в 64-битный процесс.

    Другое дело, что в 64-битный процесс нельзя загрузить 32-битные DLL (и наоборот).

    ОтветитьУдалить
  23. А никто не подскажет есть ли аналог директивы __ptr32 в Delphi?
    Если я в х64 приложении хочу явно использовать х32 указатель (для работы с DLL например), как я могу это сделать?
    Можно конечно использовать Integer или Cardinal а самому знать, что там на самом деле указатель, но с подобной директивой все-таки проще.

    ОтветитьУдалить
    Ответы
    1. А можно пример зачем такое надо?

      Удалить
    2. Пример костыльный, в реальных условиях мне сложно представить где это может понадобиться.

      Есть сторонний сервис, очень древний, естественно х32. Для него есть API (DLL из которой экспортируются методы), тоже х32. Разработчики выкатили х64 версию DLL чтобы можно было создавать полноценные х64 проекты и использовать их API при этом.
      Но часть указателей пометили __ptr32
      Да, все заголовочные файлы на С++, Delphi не поддерживается. Вот хочется использовать их API в Delphi на х64 проектах. Непонятно как перевести директиву __ptr32
      Также я не очень понимаю как быть если например: есть метод отправки сообщения, текст сообщения передается через указатель. Указатель помечен как __ptr32 Как быть если мое приложение (путь даже на С++) уже занимает больше 2GB указатель на текст записанный в памяти после никак не поместится в 32 бита? Как это разрулит компилятор С++?

      Удалить
    3. Не очень понятно как они внутри это реализовали. __ptr32 означает обрезку с 8-ми до 4-х байт. Соответственно, при разыменовании он дополняется нулями до полного указателя. Итого, если указатели выходят за 4 Гб, то нарываемся на Access Violation.

      Мне кажется, __ptr32 предназначен в основном для отладчика - чтобы ворочать 32-битными адресами в 32-битном процессе. В остальных случаях он смысла не имеет. Адреса в 64-битном процессе должны быть 64-битными, в 32-битном - 32-битными. Иначе мы нарываемся на проблему потери данных в указателе.

      Я бы, во-первых, отпинал разработчиков DLL за такой идиотизм. Во-вторых, сделал бы функцию-переходник. Пусть она принимает Pointer, делает ему Assert < 4 Gb, передаёт в настоящую функцию как DWORD/Cardinal/UInt32. И обратно: принимает DWORD/Cardinal/UInt32 - конвертирует в Pointer.

      Удалить
  24. Я кстати сделал стабы для лямбда для 64 бита, про которые спрашивал.

    http://programmingmindstream.blogspot.ru/2016/12/1321.html?m=1

    http://programmingmindstream.blogspot.ru/2016/12/1323-win64.html?m=1

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.