На самом деле это действительно интересно и помогает, когда вы полностью понимаете, как на самом деле все работает глубоко внутри компьютера. Чтобы действительно понимать язык C, вы также должны иметь глубокие знания о сборке, производимой компилятором.
На первый взгляд, давайте возьмем программу на c и попробуем поставить цель для достижения чего-то, используя знание языка ассемблера и распределения памяти. Все файлы, использованные в этом руководстве, можно найти здесь.
#include <stdio.h> int main(void) { int n; int a[5]; int *p; a[2] = 1024; p = &n; /* * write your line of code here... * Remember: * - you are not allowed to use a * - you are not allowed to modify p * - only one statement * - you are not allowed to code anything else */ /* ...so that this prints 98\n */ printf("a[2] = %d\n", a[2]); return (0); } OUTPUT: a[2] = 1024
Таким образом, нам разрешено поместить только одну строку в этот код без использования переменной a или изменения значения указателя p . Наша первая первоначальная проверка должна состоять в том, чтобы проверить размер этих объявленных переменных. Так что давайте просто проверим их в нашей программе на c, добавив эти строки в основную функцию:
printf("size of n = %d\n", sizeof(n)); printf("size of a = %d\n", sizeof(a)); printf("size of p = %d\n", sizeof(p)); printf("total = %d\n", sizeof(p)+sizeof(n)+sizeof(a)); OUTPUT: size of n = 4 size of a = 20 size of p = 8 total = 32
Таким образом, общий размер наших переменных равен 32, а массив - 4 * 5 = 20. Теперь давайте скомпилируем наш исходный код с помощью objdump и посмотрим на основной раздел кода сборки.
$ gcc file.c $ objdump -d a.out 000000000040052d <main>: 40052d: 55 push %rbp 40052e: 48 89 e5 mov %rsp,%rbp 400531: 48 83 ec 30 sub $0x30,%rsp 400535: c7 45 e8 00 04 00 00 movl $0x400,-0x18(%rbp) 40053c: 48 8d 45 d4 lea -0x2c(%rbp),%rax 400540: 48 89 45 d8 mov %rax,-0x28(%rbp) 400544: 8b 45 e8 mov -0x18(%rbp),%eax 400547: 89 c6 mov %eax,%esi 400549: bf e4 05 40 00 mov $0x4005e4,%edi 40054e: b8 00 00 00 00 mov $0x0,%eax 400553: e8 b8 fe ff ff callq 400410 <printf@plt> 400558: b8 00 00 00 00 mov $0x0,%eax 40055d: c9 leaveq 40055e: c3 retq 40055f: 90 nop NOTE: ***Only main section is shown here****
Параметр -d по умолчанию выводит инструкции в синтаксисе AT&T. Формат этого синтаксиса: мнемонический источник, место назначения. Мнемоника - это машинная инструкция, источник и назначение операндов могут содержать регистры (с префиксом %), непосредственные значения (являются константами и начинаются с префикса $ ), адреса памяти и т. д.
Стек - это область памяти для хранения элементов данных вместе с указателем на «верх» стека.
Как вы можете видеть в нашем примере выше,% rbp и% rsp относятся к категории регистров специального назначения. % rbp - это базовый указатель, который указывает на основание текущего кадра стека, а% rsp - это указатель стека, который указывает на верхнюю часть текущего кадра стека. Подробнее о стеке вызовов.
Мы можем сохранять значения в стеке, нажимая их, и что операция push уменьшает значение в регистре указателя стека, rsp. Другими словами, выделение переменных в стеке вызовов включает вычитание значения из указателя стека. Точно так же освобождение переменных из стека вызовов включает добавление значения в указатель стека.
Из этого следует, что мы можем создавать локальные переменные в стеке вызовов, просто вычитая количество байтов, требуемых каждой переменной из указателя стека. Это не сохраняет никаких данных в переменных, а просто выделяет память, которую мы можем использовать.
Поскольку% rbp указывает на базу текущего кадра стека, а% rsp указывает на вершину текущего кадра стека, чтобы действовать как база, вторая инструкция в коде сборки копирует значение% rsp в% rbp. Затем в следующей инструкции sub $ 0x30,% rsp наша программа резервирует 0x30 ячеек памяти, вычитая текущее значение на 0x30 и сохраняя его в rsp.
В следующей инструкции мы видим movl $ 0x400, -0x18 (% rbp), что в основном означает, что со значением смещения -0x18 по отношению к rbp переместите значение 0x400 (десятичное: 1024) в это адрес памяти. Что, когда мы смотрим на наш код c, напоминает a [2] = 1024;
Поскольку мы знаем, что у нас есть [5], то есть 5 элементов в нашем массиве по 4 байта каждый, мы можем знать, как он представлен в памяти.
Следующая инструкция - lea -0x2c (% rbp),% rax (lea обозначает эффективный адрес загрузки), что в основном означает, что взять адрес% rbp, сместить его на -0x2c и взять этот адрес памяти (не значение, сохраненное по этому адресу) и сохраните его в% rax. Видя оператор c p = & n, это означает, что он берет адрес n и сохраняет в% rax, а затем в следующем операторе mov% rax, -0x28 (% rbp) значение% rax сохраняется в -0x28 (% rbp), который является нашим указателем p.
Поскольку p имеет адрес n. Мы можем перейти от p к значению a [2] и изменить его. Давайте его вычислим. (0x2C-0x18) / 4 = 5, поскольку указатель будет перемещаться по 4 ячейкам памяти одновременно. Таким образом, мы можем использовать * (p + 5) = 98 в нашем коде c, чтобы изменить значение!
#include <stdio.h> int main(void) { int n; int a[5]; int *p; a[2] = 1024; p = &n; /* * write your line of code here... * Remember: * - you are not allowed to use a * - you are not allowed to modify p * - only one statement * - you are not allowed to code anything else */ *(p+5)=98; /* ...so that this prints 98\n */ printf("a[2] = %d\n", a[2]); return (0); } OUTPUT: a[2] = 98