"Подводные камни" перемещаемого кода
При разработке кода, выполняющегося в стеке, следует учитывать, что в операционных системах 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 Программа, иллюстрирующая копирование и выполнение функции в стеке