Pointer-анализ

Опубликовано: 23 Февраля, 2023

Указатели десятилетиями были занозой в боку компиляторов. Трудность заключается в сложности понимания указателей как объектов, указывающих на другие данные, и того, как они соотносятся друг с другом. Анализ указателей был неотъемлемой частью разработки компиляторов с начала 1970-х годов, и со временем было предложено множество различных подходов. В этой статье мы обсудим один подход под названием «анализ указателей», который в настоящее время считается хорошей отправной точкой для разработки систем анализа указателей (хотя и не обязательно последним словом).

Почему анализ указателей сложен?

Анализ указателей — сложная задача, поскольку указатели являются динамическими. Они могут быть как прямыми, так и косвенными, а также могут иметь псевдонимы (указатели на одну и ту же переменную). Кроме того, вы должны знать, куда указывает указатель, чтобы ваш компилятор мог сгенерировать для него эффективный код. Точки получаются из других указателей; например, если у нас есть переменная A с одной опорной точкой и другая переменная B с двумя опорными точками, то:

Одна из этих контрольных точек может быть полезна при генерации кода для A, но совершенно бесполезна при генерации кода для B. Так почему же пользователь вообще хочет, чтобы ваш компилятор генерировал код?

Вторая точка отсчета имеет значение, потому что она дает пользователю некоторую информацию о том, какой тип ячейки памяти содержит данные, на которые ссылается ваша программа, — что важно, если этот тип данных впоследствии изменится (что может легко произойти).

Вот почему так важно, чтобы ваш компилятор мог генерировать код для указателя. Пользователи не могут просто заставить компилятор транслировать указатели в адреса памяти, потому что если пользователи сделают это, то в любое время, когда кто-то изменит типы данных их программы (и, следовательно, изменит значения, содержащиеся в этих адресах), пользовательская программа сломается! Вот почему так важно уметь генерировать код для указателей.

Модель для указателей и ссылок:

Указатели и ссылки используются в C++ и Java. В C существует два типа указателей: слабые и сильные. Слабый указатель указывает на место, которое может быть уже недействительным (например, если оно было удалено из памяти). Сильные указатели всегда указывают на допустимые места в памяти.

В обоих языках существует два типа ссылок: неконстантная ссылка (также известная как обычная ссылка) и константная ссылка (также известная как константа). У этих двух концепций есть одна общая черта: они позволяют пользователям получать доступ к одному и тому же объекту с помощью различных функций или методов без дополнительной информации о том, на какой тип объекта ссылается пользователь; однако они немного отличаются, потому что непостоянные ссылки сохраняют свое значение между вызовами, а постоянные — нет».

Нечувствительность к потоку:

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

Наиболее очевидным применением этого свойства является моделирование указателей и ссылок с использованием иерархии одного и того же типа (например, int *int). Однако его также можно использовать для других целей, таких как представление общих подвыражений или оценка выражения во время выполнения (например, если у пользователя есть массив с пятью элементами, и он хочет знать, содержится ли в нем больше трех элементов).

Второе свойство — это косвенность указателя, которая говорит, что указатель может указывать на другой указатель. Это свойство позволяет нам моделировать рекурсивные структуры, такие как деревья, с помощью указателей.

Третье свойство — это псевдоним, который говорит о том, что два указателя могут указывать на один и тот же объект. Это свойство позволяет нам моделировать разделяемую память с помощью указателей.

Четвертое свойство — это свойство изменчивости, которое говорит о том, что указатель можно изменить. Это свойство позволяет нам моделировать изменяемые объекты, такие как массивы и строки, с помощью указателей.

Пятое свойство — это нулевой указатель, который представляет собой недопустимый указатель. Это особое значение, которое не может быть разыменовано и не может ни на что указывать. Его можно использовать для представления пустых списков, пустых наборов и других объектов, не имеющих элементов или элементов.

Формулировка в журнале данных:

Datalog — это язык логического программирования, который можно использовать для моделирования и решения проблем во многих областях. Формула для анализа указателя в журнале данных используется для анализа манипуляций с указателем в программе. Это помогает нам понять, какие части программы необходимо обновить, когда мы вносим изменения в указатели или ссылки.

Формула утверждает, что если у пользователя есть две программы P1 и P2, то:

For every transition T from state A(x) => B(y). 
If P1(A)  → B(y) then there must exist another transition T" such that (P1(T")-P2(T)) > 0
If P2(A)  → B(y) then there must exist another transition T" such that (P1(T")-P2(T)) < 0

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

Рассмотрим следующий пример: Программа содержит две ссылки: ref1 и ref2. Они оба инициализированы нулем, что означает, что ни одна из ссылок не имеет никакого связанного с ними значения. Затем мы модифицируем ref1, чтобы он указывал на объект типа T, и инициализируем ref2 другой ссылкой r. Мы хотим знать, каковы последствия этих изменений. Мы можем использовать формулы анализа указателя, чтобы выяснить это.

Первая формула говорит нам, что если мы делаем переход из состояния A(null) ⇒ B(T), то должен существовать другой переход из состояния B(T) => C. Вторая формула говорит нам, что нет никакого пути для программе достичь состояния C, если она уже не достигла состояния B.

Использование информации о типе:

Использование информации о типе для построения модели программы очень полезно при разработке компилятора.

Компилятор может использовать эту информацию, чтобы определить, является ли указатель безопасным для разыменования и, следовательно, имеет ли смысл преобразовывать его в ссылку на объект (например, путем вызова malloc).

Он также может использовать информацию о типе, чтобы определить, допустим ли указатель в качестве аргумента для memcpy() или memcmp() . В обоих случаях, если в стеке нет разыменовываемого объекта, который соответствует тому, что пользователь передал в качестве исходного параметра, то это означает, что любое значение, которое пользователь копирует (или сравнивает), вместо этого копируется в память куда-то еще; нет смысла делать эту операцию вообще!

Информация о типе позволяет нам узнать, когда глобальные переменные размещаются в стеке, в куче или в обоих; это помогает нам избежать написания кода, который приведет к сбою при попытке доступа к таким переменным позже в нашей программе, если они еще не будут правильно выделены из-за того, что мы не знали об их существовании заранее во время компиляции, а только после загрузки всего в память перед повторным запуском выполнения, что может легко произойти, если разные разделы в наших программах будут скомпилированы отдельно, без предварительного связывания их вместе!

Вывод:

В этой статье мы обсудили некоторые проблемы, с которыми мы сталкиваемся при выполнении анализа указателей. Так как есть много разных способов решения этих проблем. Мы также рассмотрели некоторые стратегии решения этих проблем, но, в конечном счете, каждый отдельный автор компилятора должен решить, какой подход лучше всего подходит для нужд его собственного проекта.