Олег Марков
Сегмент constants - constants-segment в бинарных форматах и рантаймах
Введение
Сегмент constants (часто называемый constants-segment, .rodata, .rdata, .const) — это область в бинарном файле или в памяти процесса, где размещаются неизменяемые данные: строковые литералы, константные массивы, таблицы, иногда — предвычисленные значения.
Когда вы пишете в коде:
// Строковый литерал и константный массив
const char* msg = "Hello";
const int table[3] = {1, 2, 3};
компилятор почти всегда разместит строку "Hello" и массив table в сегменте constants. В итоге:
- эти данные могут быть размещены в только для чтения памяти;
- загрузчик операционной системы может делить их между несколькими процессами;
- компилятору проще выполнять оптимизации, зная, что значения никогда не меняются.
В этой статье мы шаг за шагом разберем, как устроен сегмент constants / constants-segment, как он выглядит в ELF (Linux, Unix-подобные системы), в PE (Windows), а также обсудим общие принципы, которые встречаются в разных языках и рантаймах (C/C++, Rust, Go, JVM и др.). Я буду опираться на практические примеры и показывать, что реально попадает в constants-segment и как это можно увидеть.
Что такое сегмент constants по сути
Логическое и физическое разделение
Важно разделять:
- логический уровень языка — вы пишете
const,static, литералы и ожидаете, что это «константы»; - физический уровень бинаря — реальные сегменты и секции в формате ELF/PE, которые попадают в память процесса.
Сегмент constants — это физическая область:
- в ELF это, как правило, секции
.rodata,.rodata.rel.ro, иногда часть.text(для встроенных таблиц) и соответствующие им сегменты; - в PE это секции
.rdata,.text(inline-константы) и другие только читаемые области.
Смотрите, как это выглядит на примере Linux ELF.
Основные свойства constants-segment
Если кратко, constants-segment обычно:
- Только для чтения (флаг
READ, безWRITE). - Иногда совместно используемый между процессами (shared) за счет механизма copy-on-write.
- Выравнивание (alignment) часто большее, чем у обычных данных, для ускорения доступа.
- Подходит для сжатия и дедупликации при линковке и загрузке.
Благодаря этому:
- попытка записи в эту память может привести к падению процесса (segmentation fault / access violation);
- один экземпляр строковых литералов может использоваться всеми процессами, которые загрузили одинаковую библиотеку.
Сегмент constants в ELF (Linux, Unix)
Основные секции: .rodata и .rodata.rel.ro
В ELF-файлах вы чаще всего встретите такие секции:
.rodata— «read-only data», неизменяемые константы;.rodata.rel.ro— секция, которая после релокации становится только для чтения;- иногда
.eh_frame,.gcc_except_tableи др. — служебные «константные» таблицы для исключений и отладочной информации.
Давайте разберемся на небольшом примере.
// file: main.c
#include <stdio.h>
const char* msg = "Hello constants segment";
const int table[3] = {10, 20, 30};
int main() {
// Здесь мы читаем данные из сегмента constants
printf("%s\n", msg); // msg указывает на строку в .rodata
printf("%d\n", table[1]); // table лежит в .rodata
return 0;
}
Скомпилируем:
gcc -O2 main.c -o main
Теперь вы можете посмотреть, где лежат эти объекты:
nm -S main | grep -E 'msg|table'
Комментарии к результату:
msgбудет помечен какDилиRв зависимости от компоновки (данные, только чтение);- массив
tableтоже окажется в.rodataили.data.rel.ro.
Просмотр секций через readelf
Вы можете увидеть секции, где лежат константы:
readelf -S main | grep rodata
Типичный вывод:
[12] .rodata PROGBITS 0000000000402000 ...
[13] .rodata.rel.ro PROGBITS 0000000000403000 ...
.rodata— обычные константы, которые не требуют релокации;.rodata.rel.ro— данные, которые сначала нужно отрелоцировать загрузчиком, а затем секция становится read-only.
Например, адреса виртуальных таблиц C++ часто попадают в .rodata.rel.ro, так как содержат указатели, которые надо скорректировать при загрузке.
Отображение в сегменты (program headers)
На уровне program headers (readelf -l main) вы увидите сегмент типа LOAD:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00400000 0x00400000 0x20000 0x20000 R E 0x200000
LOAD 0x020000 0x00600000 0x00600000 0x10000 0x10000 R 0x200000
LOAD 0x030000 0x00700000 0x00700000 0x10000 0x10000 RW 0x200000
- во втором сегменте
RбезWкак раз могут находиться.rodata,.eh_frameи другие константные данные; - загрузчик помечает виртуальные страницы этого сегмента как только для чтения.
Сегмент constants в PE (Windows)
Секция .rdata
В форматах PE (исполняемые файлы и DLL под Windows) основная секция для констант — .rdata:
- строковые литералы C, C++;
- RTTI (данные для
dynamic_cast,typeid); - константные таблицы.
Вот пример кода, аналогичный предыдущему, но под Windows (MSVC):
// file: main.cpp
#include <iostream>
const char* msg = "Hello constants segment in PE";
const int table[3] = {100, 200, 300};
int main() {
// Здесь мы читаем данные из constants-segment
std::cout << msg << std::endl; // msg указывает в .rdata
std::cout << table[2] << std::endl; // table лежит в .rdata
return 0;
}
Скомпилировав этот код с /MD /O2, вы сможете при помощи утилит (например, dumpbin /headers) увидеть секцию .rdata:
SECTION HEADER #3
.rdata name
...
Flags: read only initialized data
- Секция
.rdataпомечена как «read only» — она попадает в constants-segment. - При попытке записи в эту память вы получите
Access violation.
Какие данные попадают в constants-segment
Строковые литералы
Строка вида "Hello world" почти всегда попадает в .rodata / .rdata. Для примера:
// Комментарий - строка хранится в constants-segment
const char* hello = "Hello world";
- Сам указатель
helloможет лежать в.dataили.bss(в зависимости от хранения); - Сама строка
"Hello world"почти всегда в constants-segment.
Нюанс: в некоторых случаях компилятор может:
- объединять одинаковые строки (string pooling);
- размещать строки прямо в
.text(реже, при специальных опциях).
Константные массивы и таблицы
Если массив объявлен как const и не требует модификации, он попадает в сегмент constants:
// Это хороший кандидат для placement в .rodata
const int sin_table[360] = {
// Здесь могут быть предвычисленные значения синуса
};
Такие предвычисленные таблицы активно используются в:
- криптографических алгоритмах;
- мультимедиа кодеках;
- численных методах.
Константные глобальные переменные
Переменные, объявленные как const на уровне модуля, компилятор старается положить в read-only секции:
// Глобальная константа
const double Pi = 3.141592653589793;
На практике:
- в C++
constпо умолчанию имеет внутреннее связывание (internal linkage), что позволяет компилятору поместить его в.rodataи иногда даже удалить как отдельный объект, подставляя значение напрямую; - в C поведение отличается, и часто приходится явно указывать
const+static, чтобы избежать внешнего символа.
Таблицы виртуальных функций и RTTI
В C++ многие служебные структуры (vtable, RTTI) хранятся в только читаемых областях:
class Base {
public:
virtual void foo() {}
};
class Derived : public Base {
public:
void foo() override {}
};
- таблички виртуальных вызовов (vtable) нередко лежат в
.rodata.rel.ro(ELF) или.rdata(PE); - это позволяет защитить их от модификации в рантайме, усложняя некоторые классы атак.
Как посмотреть, что именно в constants-segment
Через objdump / readelf (ELF)
Давайте посмотрим содержимое .rodata на примере:
objdump -s -j .rodata main
Комментарии:
-s— показать полный дамп содержимого секции;-j .rodata— ограничиться только секцией.rodata.
Вы увидите байты и справа — интерпретацию как текст, где можно найти знакомые строки.
Если хотите узнать, какой символ лежит в какой секции:
readelf -s main | grep my_const_name
- так вы увидите, в какой секции секции находится символ.
Через dumpbin / objdump (PE)
Под Windows можно использовать:
dumpbin /headers main.exe
dumpbin /section:.rdata main.exe
или GNU objdump для PE:
objdump -s -j .rdata main.exe
Таким образом вы можете убедиться, что ваши константы реально размещены в read-only секции.
constants-segment и механизм защиты памяти
Флаги страниц и защита от записи
Когда загрузчик ОС мапит файл в память, он использует флаги:
READ— разрешено чтение;WRITE— разрешена запись;EXECUTE— разрешен запуск кода.
Сегмент constants обычно получает флаги:
- ELF:
R(иногдаRE, если в том же сегменте есть код); - PE:
IMAGE_SCN_MEM_READбезIMAGE_SCN_MEM_WRITE.
Если вы попытаетесь записать в такую область:
// Опасный код - попытка записи в константу
extern const int table[3];
void bad_write() {
// Здесь мы явно нарушаем правило - пишем в const
int* p = (int*)table; // Приводим к неконстантному указателю
p[0] = 42; // Запись в constants-segment
}
- такой код формально является undefined behavior на уровне C/C++;
- на практике он может привести к
segmentation faultилиaccess violation, потому что страница памяти только для чтения.
Copy-on-write и разделяемость
Если библиотека или исполняемый файл загружены в несколько процессов:
- их constants-segment может быть общим (shared) между процессами;
- операционная система не создает отдельную копию этих страниц для каждого процесса, если нет записи.
Это важно:
- экономится память при запуске десятков процессов, использующих одну и ту же библиотеку;
- особенно выгодно для больших наборов константных данных (например, словари, таблицы, шрифты и т.п.).
constants-segment и языки программирования
Здесь я покажу, как разные языки и рантаймы используют constants-segment, чтобы вам было проще связать теорию с реальными проектами.
C и C++
В C/C++ сегмент constants связан с:
- ключевым словом
const; - строковыми и числовыми литералами;
- спецификаторами хранения (
static,extern) и линковкой.
Посмотрите на такой пример:
// file: consts.c
#include <stdio.h>
// Глобальная константа - кандидат в constants-segment
const int GLOBAL_CONST = 123;
// Константный массив - тоже кандидат
const int GLOBAL_TABLE[4] = {1, 2, 3, 4};
int main() {
// Читаем значения из constants-segment
printf("%d\n", GLOBAL_CONST); // Чтение константы
printf("%d\n", GLOBAL_TABLE[2]); // Чтение элемента массива
return 0;
}
Компилятор может:
- поместить
GLOBAL_CONSTиGLOBAL_TABLEв.rodata; - иногда вообще не сохранять
GLOBAL_CONSTкак отдельный объект, а подставлять123напрямую в инструкции.
Внутреннее и внешнее связывание
Если вы напишете:
// Внутреннее связывание - виден только внутри translation unit
static const int INTERNAL_CONST = 7;
- такая константа почти всегда попадет в
.rodataи не будет видна другим объектным файлам.
А вот так:
// Внешнее связывание - может потребоваться отдельный символ
extern const int EXTERNAL_CONST;
- в этом случае линкер может вынуждено создать символ в таблице, а реализация (определение) EXTERNAL_CONST тоже будет в какой-то секции, часто
.rodataили.data.rel.ro.
Rust
В Rust есть два близких механизма:
const— константа, подставляемая на этапе компиляции;static— статическая переменная (может бытьmut, может бытьconst).
// file: main.rs
// Константа компиляции - может вообще не попасть в память как объект
const PI: f64 = 3.141592653589793;
// Статик - реальный объект в памяти
static GREETING: &str = "Hello from constants-segment";
fn main() {
// Здесь компилятор может подставить 3.14... напрямую в инструкции
let circle = 2.0 * PI;
// А GREETING будет ссылаться на строку в сегменте констант
println!("{}", GREETING);
}
- строка
"Hello from constants-segment"попадет в.rodata; GREETINGокажется в одной из статических секций, а его значение будет указывать на константную строку.
Go
В Go constants-segment менее явно виден, но под капотом:
- строковые литералы размещаются в только читаемых секциях;
- глобальные неизменяемые данные (например, таблицы в runtime) тоже могут жить в constants-segment.
Пример:
// file: main.go
package main
import "fmt"
// Глобальная константа - на уровне Go это compile-time константа
const GlobalConst = 42
// Глобальная переменная с литералом - ее данные могут уйти в constants-segment
var greeting = "Hello from Go constants-segment"
func main() {
// GlobalConst подставляется на этапе компиляции
fmt.Println(GlobalConst)
// greeting ссылается на строку, хранящуюся в read-only сегменте
fmt.Println(greeting)
}
В Go вы редко напрямую работаете с ELF/PE, но важно понимать, что:
- константы и строковые литералы хранятся в специальных таблицах;
- runtime и компилятор используют концепцию сегмента констант и внутренние структуры (например, для интерфейсных типов, отображения типов и т.д.).
JVM (Java, Kotlin и др.)
В мире JVM есть особая структура — constant pool (пул констант) в .class-файлах и в памяти виртуальной машины:
- это логический аналог constants-segment;
- в нем хранятся строковые константы, числовые значения, ссылки на методы и поля.
Пример кода:
// file: Main.java
public class Main {
static final String GREETING = "Hello constants segment in JVM";
public static void main(String[] args) {
// GREETING лежит в constant pool и/или в сегменте констант JVM
System.out.println(GREETING);
}
}
- JVM по-своему размещает этот pool в памяти;
- многие реализации помечают такие данные как только для чтения, чтобы защитить их и делить между классами.
Оптимизации, связанные с constants-segment
Константная свертка и подстановка значений
Когда значение известно на этапе компиляции, компилятор может:
- не размещать его в constants-segment;
- напрямую подставить в инструкцию.
Например:
const int N = 10;
int arr[N];
- здесь
Nизвестен на этапе компиляции; - он используется для определения размера массива и может вообще не появиться как отдельный объект.
С другой стороны:
const int N = 10;
int getN() {
return N; // Здесь может потребоваться реальное хранение
}
- компилятор может либо подставить
10, либо оставить символNв.rodataи грузить его при выполнении.
Дедупликация строк и констант
Компиляторы и линкеры могут:
- объединять одинаковые строки и константы в одну копию;
- использовать специальные секции (например,
.rodata.cst8для 8-байтных констант в некоторых реализациях).
Давайте посмотрим маленький пример:
// Одинаковые строки - кандидат на дедупликацию
const char* a = "same literal";
const char* b = "same literal";
- в итоговом бинаре может быть только одна запись
"same literal"в сегменте констант; aиbбудут указывать на один и тот же адрес.
Это уменьшает размер бинаря и экономит память.
Размещение таблиц для быстрого доступа
Большие константные таблицы (например, S-box в AES, таблицы CRC и т.д.) выгодно класть в constants-segment:
- они не меняются;
- их можно разместить с хорошим выравниванием;
- можно использовать оптимизированный доступ с учетом кэша.
Пример:
// Таблица для CRC32 - изменять ее нельзя
static const unsigned int crc32_table[256] = {
// Здесь находится предвычисленная таблица CRC
};
- такая таблица почти всегда уходит в
.rodata; - попытка ее модификации — UB и, скорее всего, ошибка времени выполнения.
Практические рекомендации по работе с constants-segment
Явно отделяйте константы от изменяемых данных
Если вы хотите, чтобы данные попали в constants-segment:
- помечайте их как
const(C/C++),staticбезmut(Rust),final(Java) и т.д.; - не модифицируйте их ни прямо, ни через приведение типов.
Пример в C:
// Хороший стиль - явная константность
static const char error_message[] = "Fatal error occurred";
// Плохой стиль - данные в .data, их могут случайно изменить
static char mutable_error_message[] = "Fatal error occurred";
Избегайте «насильной» записи в const
Код вроде:
const int X = 5;
void bad() {
int* p = (int*)&X; // Снимаем const через cast
*p = 7; // Пишем в constants-segment
}
- нарушает правило только для чтения;
- может привести к непредсказуемому поведению.
Если вам нужно менять значения — сделайте их явно изменяемыми и не полагайтесь на нарушенные инварианты.
Используйте константные таблицы вместо магических чисел
Если в коде много «магических» чисел, которые вы регулярно используете:
- лучше вынести их в константную таблицу в constants-segment;
- это упростит поддержку и иногда улучшит локальность данных.
Реальный пример:
// Вынесенные коэффициенты фильтра в constants-segment
static const float filter_coeffs[5] = {
0.1f, 0.15f, 0.5f, 0.15f, 0.1f
};
float apply_filter(const float* data) {
float acc = 0.0f;
for (int i = 0; i < 5; ++i) {
// Здесь мы многократно читаем из constants-segment
acc += data[i] * filter_coeffs[i];
}
return acc;
}
Следите за размером сегмента констант
Если вы загружаете:
- большие словари;
- встроенные ресурсы (картинки, шрифты, шаблоны);
- предвычисленные таблицы,
они увеличивают размер constants-segment. Это:
- возможно, увеличит размер бинаря;
- повлияет на время загрузки приложения.
В некоторых случаях целесообразно:
- вынести крупные данные во внешние файлы и грузить их по требованию;
- сжимать данные и распаковывать в runtime в обычную память.
Принудительное размещение в определенной секции (продвинуто)
Иногда вам нужно, чтобы константа оказалась в конкретной секции / сегменте. Например, для встроенных систем или особых загрузчиков.
В C/C++ на GCC/Clang можно использовать атрибут section:
// Размещаем константу в специально названной секции
const char custom_msg[] __attribute__((section(".my_rodata"))) =
"Hello from custom constants-segment";
Комментарии:
- линкер-скрипт может сопоставить
.my_rodataс конкретным сегментом памяти; - это полезно, когда у вас несколько областей flash/ROM и нужно точно контролировать расположение.
В MSVC есть аналогичные механизмы (#pragma section, __declspec(allocate(...)) и т.п.).
Заключая, сегмент constants / constants-segment — это фундаментальный механизм, который связывает ваш исходный код с реальной памятью машины. Когда вы понимаете, как и где живут константы:
- проще анализировать использование памяти;
- легче диагностировать ошибки «запись в read-only память»;
- появляется возможность тонкой оптимизации и работы с защитой от атак.
Покажу вам простое резюме:
- строковые и числовые литералы, константные массивы и служебные таблицы рантайма обычно живут в constants-segment;
- этот сегмент помечен как read-only, может быть разделяемым между процессами;
- вы можете видеть его через
.rodata,.rdataи другие секции ELF/PE; - корректное использование
constи аналогов помогает компилятору и ОС эффективно работать с вашей программой.
Частозадаваемые технические вопросы
Как проверить, что конкретная переменная точно попала в constants-segment
- Скомпилируйте программу без удаления символов (без лишних
strip). - Найдите символ через
nm(ELF) илиdumpbin /symbols(PE).
Пример для ELF:bash nm -S a.out | grep MY_CONST - Посмотрите секцию через
readelf -s a.outиreadelf -S a.out.
Если секция.rodata,.rodata.rel.roили.rdata— это constants-segment.
Почему const-переменная иногда оказывается в .data а не в .rodata
Чаще всего это происходит когда:
- переменная требует релокации и компоновщик не может сделать ее полностью read-only до загрузки;
- вы используете особые атрибуты или опции линкера;
- в C она имеет внешнее связывание и компилятор оставляет ее в изменяемом сегменте.
Проверьте модификаторыstaticиexternи включите оптимизации (-O2), тогда компилятор чаще выносит такие объекты в.rodata.
Как запретить дедупликацию строк и констант при линковке
В GCC/Clang дедупликация строк контролируется опциями:
-fmerge-constants,-fno-merge-constants;-fmerge-all-constants.
Если вам нужно сохранить разные копии одинаковых строк (например, для отладки), используйте -fno-merge-constants или компиляторские атрибуты, отключающие объединение для конкретного объекта.
Можно ли разместить в constants-segment данные которые нужно изменить только один раз при старте
Полностью нет — по определению constants-segment только для чтения.
Но можно использовать схему:
- Данные лежат в read-only сегменте как «образ по умолчанию».
- При старте вы копируете их в обычную read-write память.
- Работаете уже с копией.
Это распространенный прием в embedded: константы в flash, копия в RAM.
Как управлять размером constants-segment при использовании больших ресурсов
Рекомендуемая стратегия:
- Не встраивайте крупные бинарные ресурсы напрямую в код если это не обязательно.
- Если нужно встраивать — сжимайте их (например, используя
gzipи встроенный декодер). - Для ELF отследите размер
.rodataчерезsizeилиreadelf -S. - Если сегмент слишком большой — вынесите часть ресурсов во внешние файлы и загружайте динамически.