Сегмент constants - constants-segment в бинарных форматах и рантаймах

05 января 2026
Автор

Олег Марков

Введение

Сегмент 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 обычно:

  1. Только для чтения (флаг READ, без WRITE).
  2. Иногда совместно используемый между процессами (shared) за счет механизма copy-on-write.
  3. Выравнивание (alignment) часто большее, чем у обычных данных, для ускорения доступа.
  4. Подходит для сжатия и дедупликации при линковке и загрузке.

Благодаря этому:

  • попытка записи в эту память может привести к падению процесса (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

  1. Скомпилируйте программу без удаления символов (без лишних strip).
  2. Найдите символ через nm (ELF) или dumpbin /symbols (PE).
    Пример для ELF: bash nm -S a.out | grep MY_CONST
  3. Посмотрите секцию через 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 только для чтения.
Но можно использовать схему:

  1. Данные лежат в read-only сегменте как «образ по умолчанию».
  2. При старте вы копируете их в обычную read-write память.
  3. Работаете уже с копией.

Это распространенный прием в embedded: константы в flash, копия в RAM.

Как управлять размером constants-segment при использовании больших ресурсов

Рекомендуемая стратегия:

  1. Не встраивайте крупные бинарные ресурсы напрямую в код если это не обязательно.
  2. Если нужно встраивать — сжимайте их (например, используя gzip и встроенный декодер).
  3. Для ELF отследите размер .rodata через size или readelf -S.
  4. Если сегмент слишком большой — вынесите часть ресурсов во внешние файлы и загружайте динамически.
Стрелочка влевоСегмент hooks - как использовать hooks-segment в React проектахСегмент components - компонент components-segment в современных интерфейсахСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий