ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

       

"Подводные камни" перемещаемого кода


При разработке кода, выполняющегося в стеке, следует учитывать, что в операционных системах Windows 9x, Windows NT и Windows 2000 местоположение стека различно, и, чтобы сохранить работоспособность при переходе от одной системы к другой, код должен быть безразличен к адресу, по которому он будет загружен. Такой код называют перемещаемым,

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

Замечательно, что у микропроцессоров серии Intel 80x86 все короткие переходы (short jump) и близкие вызовы (near call) относительны, т.е. содержат не линейный целевой адрес, а разницу целевого адреса и адреса следующей выполняемой инструкции. Это значительно упрощает создание перемещаемого кода, но вместе с этим накладывает на него некоторые ограничения.

Что произойдет, если следующую функцию "void Demo() { printf("Demo\n");}" скопировать в стек и передать ей управление? Поскольку, инструкция call, вызывающая функцию pritnf, "переехала" на новое место, разница адресов вызываемой функции и следующей за call

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

Программируя на ассемблере, такое ограничение можно легко обойти, используя регистровую адресацию. Перемещаемый вызов функции printf упрощенно может выглядеть, например, так:"lea eax, printf\ncall eax." В регистр eax (или любой другой регистр общего назначения) заносится абсолютный линейный, а не относительный адрес и, независимо от положения инструкции call, управление будет передано функции printf, а не чему-то еще.

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

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

void Demo(int (*_printf) (const char *,...) )

{

       _printf("Hello, Word!\n");

       return;

}

int main(int argc, char* argv[])

{

       char buff[1000];

       int (*_printf) (const char *,...);

       int (*_main) (int, char **);

       void (*_Demo) (int (*) (const char *,...));    

       _printf=printf;

       int func_len = (unsigned int) _main - (unsigned int) _Demo;

       for (int a=0;a<func_len;a++)

              buff[a]= ((char *) _Demo)[a];

       _Demo = (void (*) (int (*) (const char *,...)))  &buff[0];

       _Demo(_printf);

       return

0;

}

Листинг 3 Программа, иллюстрирующая копирование и выполнение функции в стеке


Содержание раздела