• Партнер

  • Прогресс идет семимильными шагами. Технологии развиваются, и вместе с ними меняется наша жизнь. Еще совсем недавно 2 Гб оперативной памяти считалось верхом производительности. Но вот наступила эра 64-битных процессоров, и возможности современных ПК расширились в сотни раз. Но чтобы раскрыть весь потенциал новой архитектуры, нужны правильные программисты. Сегодня мы узнаем об основных ошибках, с которыми может столкнуться простой 32-битный кодер в новых системах.

    Для начала давай определимся, что же такое эта загадочная 64-битность. Существуют две наиболее известные 64-битные архитектуры – это IA64 и Intel 64 (или AMD64/x86-64/x64). Первая является совместной разработкой Intel и Hewlett Packard и реализована в микропроцессорах Itanium и Itanium 2. Вторая же представляет собой расширение архитектуры x86 с полной обратной совместимостью. Благодаря этой совместимости x86-64 пользуется большей популярностью, чем IA64. Основными достоинствами x64 является 64-битное адресное пространство, расширенный набор регистров и набор команд, привычный для разработчиков, а также возможность запуска 32-битных ОС и приложений.

    Практически все современные ОС поддерживают 64 бита, а Microsoft проводила такие эксперименты еще в годы расцвета Windows XP. Благодаря специальному режиму WoW64 (Windows-on-Windows 64), который транслирует вызовы 32-битных приложений к ресурсам 64-битной операционной системы, в винде могут корректно работать как 64-битные приложения, так и приложения, заточенные под архитектуру x86.

    Стоит немного рассказать и об адресном пространстве в Intel 64, поскольку именно с ним связанно большинство типичных ошибок при переходе от 32-битного программирования к 64-битному. Процессоры x64 теоретически могут адресовать до 16 экзобайт памяти, но на практике это число оказывается гораздо меньше – 16 терабайт. Более того, в связи с маркетинговыми соображениями, максимальный предел оперативки изменяется в зависимости от версии винды. Так, например Windows 7 Home Basic может адресовать до 8 Гб памяти, а Windows 7 Ultimate – до 192 Гб.

    Теперь, когда мы имеем определенное представление о 64-битности, пора рассмотреть основные ошибки, совершаемые неопытными в x64-программировании кодерами.

     

    Виртуальные функции

    Пусть у нас есть код, который разрабатывался в Visual Studio 6. В этом коде мы определили класс CSampleApp, который наследуется от CWinApp. В дочернем классе мы перекрываем функцию WinHelp, которая определена как виртуальная. Выглядит это примерно так:

    Виртуальная функция WinHelp

    class CWinApp
    {
    virtual void WinHelp(DWORD dwData, UINT nCmd);
    }

    class CSampleApp: public CWinApp
    {
    virtual void WinHelp(DWORD dwData, UINT nCmd);
    }

    Затем проект перекочевывает в Visual Studio 2005, где прототип функции WinHelp в базовом классе CWinApp изменился. Первый параметр метода теперь имеет тип DWORD_PTR, меж тем как в дочернем классе он так и остался DWORD.

    Проблема с прототипами функции

    class CWinApp
    {
    // тип первого параметра изменился
    virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
    }

    class CSampleApp
    {
    virtual void WinHelp(DWORD dwData, UINT nCmd);
    }

    Этот код будет прекрасно работать, будучи скомпилированным под 32-архитектуру, но если мы попробуем собрать программу для x64-системы, то получим странный эффект – две разные функции WinHelp. Связано это с тем, что в 64-битном кодинге тип DWORD не равен типу DWORD_PTR, и компилятор в дочернем классе генерирует две копии одной и той же функции, а не перекрывает ее, как ожидается.

    Такой тип ошибок нельзя предвидеть заранее, ведь подобное недоразумение может случиться не только при использовании классов MFC, но и вообще с любым кодом. Единственный способ избежать неприятностей – это внимательно анализировать код.

     

    Перегруженные функции

    После виртуальных функций поговорим немного о перегруженных. Если в программе есть функция, перекрытая для 32-битных и 64-битных значений, то обращение к ней, например, с типом size_t, будет транслироваться в различные вызовы. Примерно так:

    Перегруженные функции

    static void NumOfBits(const unsigned __int32 &)
    {
    printf(“32-битный параметр”);
    }
    static void NumOfBits(const unsigned __int64 &)
    {
    printf(“64-битный параметр”);
    }

    Код, собранный для x86 процессора, будет вызывать первую версию функции, а для x64 – вторую. В некоторых ситуациях такое поведение может быть очень удобным. Но тут есть и свои опасности.

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

    Класс стека и его неправильное использование

    class MyStack {
    ...
    public:
    void Push(__int32 &);
    void Push(__int64 &);
    void Pop(__int32 &);
    void Pop(__int64 &);
    }

    MyStack stack;
    // в x64 системе кладем в стек 8 байт
    ptrdiff_t value1;
    stack.Push(value1);

    // а достаем 4!!!
    int value2;
    stack.Pop(value2);

    Тут мы кладем в стек значение типа ptrdiff_t, а достаем уже int. Под x86 величина этих типов одинакова, но в архитектуре Intel 64 ptrdiff_t ровно в два раза больше старого-доброго int. Таким образом, мало того, что мы будем получать из стека неправильные значения, так еще и организуем себе утечки памяти. Поэтому надо внимательнее относиться к перегруженным функциям, передавая им аргументы правильных типов.

     

    Константы

    За много лет программирования под 32-битные платформы кодеры привыкли использовать константы, которые участвуют в операциях вычисления адреса, размера объектов или в битовых операциях. Эти константы могут сильно усложнить жизнь при переносе проекта на x64-систему. Вот несколько примеров, которые прекрасно работают в x86-операционках, но приведут к плачевным результатам на 64-битной машине.

    Константы, которые ломают 64-битные программы

    // Пример 1
    size_t ArraySize = N * 4;
    intptr_t *Array = (intptr_t *)malloc(ArraySize);

    // Пример 2
    size_t values[ARRAY_SIZE];
    memset(values, 0, ARRAY_SIZE * 4);

    // Пример 3
    size_t n, r;
    n = n >> (32 - r);

    Код первого примера выделяет память под массив указателей, считая, что размер каждого элемента массива равен 4 байтам. Все будет прекрасно работать под 32 битами, но вот в Intel 64 мы получим «out of memory». Во втором примере мы тоже считаем размер size_t равным 4 байтам, но размерность этого типа зависит от количества бит в системе. Ну, а третий пример демонстрирует ошибку, связанную с числом бит, содержащихся в size_t.

    Исправить эти баги можно, внимательно проанализировав код и заменив подобные константы на sizeof() и специальные значения из <limits.h>, <inttypes.h> и т.д.

    Исправляем ошибки с размерностью

    // Пример 1
    size_t ArraySize = N * sizeof(intptr_t);
    intptr_t *Array = (intptr_t *)malloc(ArraySize);

    // Пример 2
    size_t values[ARRAY_SIZE];
    memset(values, 0, ARRAY_SIZE * sizeof(size_t));

    // Пример 3
    size_t n, r;
    n = n >> (CHAR_BIT * sizeof(n) - r);

    Еще один способ потратить долгие часы на отладку кода в 64-битных системах – это использовать константу вида const size_t M = 0xFFFFFFF0u. Обычный 32-битный кодер, записывая такое выражение, предполагает, что создает константу, все биты которой, кроме четырех последних, равны единице. К сожалению, под x64 значение M будет совсем другое – 0x00000000FFFFFFF0u. Исправить это можно, используя либо #ifdef, либо специальный макрос.

    Решаем проблему с константой 0xFFFFFFF0u

    #ifdef _WIN64
    #define CONST3264(a) (a##i64)
    #else
    #define CONST3264(a) (a)
    #endif

    const size_t M = ~CONST3264(0xFu);

    Очень часто в качестве кода ошибки или другого специального маркера используют значение -1. И практически всегда его записывают как 0xFFFFFFFF. Но на 64-битных платформах это число вовсе не означает «минус единицу», и следующий пример это наглядно демонстрирует:

    Возвращаем код ошибки

    #define INVALID_RESULT (0xFFFFFFFFu)
    size_t UserStrLen(const char *str)
    {
    if (str == NULL)
    return INVALID_RESULT;
    ...
    return n;
    }

    size_t len = UserStrLen(str);

    // в 64-битной системе обработка ошибки никогда не будет выполнена
    // даже если ошибка действительно имела место
    if (len == (size_t)(-1))
    // обработка ошибки

    Тело if выполнится только в 32-битной среде, а в x64 сравнение в условии оператора будет ложно. Если архитектура приложения не позволяет отказаться от использования кодов ошибок, то корректная запись INVALID_RESULT, которая будет правильно работать и в 32-, и в 64-битном окружениях, будет выглядеть примерно так: #define INVALID_RESULT (size_t(-1)).

     

    Заключение

    Конечно, это лишь краткий обзор типовых проблем при переходе с 32-битной архитектуры на 64-битную. Но все описанные случаи основываются на увеличении размерности некоторых типов и поэтому, после определенной тренировки такие ошибки будут отлавливаться и предотвращаться сами собой, надо лишь накопить немного опыта.

     

    Тип ptrdiff_t

    ptrdiff_t – базовый знаковый целочисленный тип языка C/C++. Размер типа выбирается таким образом, что на 32-битной системе он равен 4 байтам, а на 64-битной – 8. То есть, в переменную этого типа может быть безопасно помещен практически любой указатель. ptrdiff_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики.

    Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии