Адресация в памяти и работа с указателями в языке Си

Бесплатная консультация юриста

+7 (800) 301-07-30

Реклама jurik.ru

Понятие «адрес части» является фундаментальным в информатике‚ но его смысл кардинально меняется в зависимости от контекста: работаем ли мы с оперативной памятью (низкий уровень‚ указатели‚ ассемблер)‚ виртуальной памятью процесса (сегменты‚ смещения) или сетевыми ресурсами (URI/URL). В этой статье мы подробно разберем основные сценарии определения адресов‚ опираясь на архитектуру ЭВМ‚ языки Си/Ассемблер и сетевое программирование.


Адресация в памяти: базовые концепции

Прежде чем узнавать адрес‚ нужно понять‚ что он собой представляет. В архитектуре фон Неймана память — это линейный массив ячеек‚ каждая из которых имеет уникальный номер — адрес.

Физический‚ линейный и логический адреса

  • Физический адрес — сигнал на адресной шине процессора‚ выбирающий конкретную ячейку ОЗУ.
  • Линейный адрес — адрес в плоском адресном пространстве (в 32-битном режиме до 4 ГБ). В режиме реального адресации или при отключенной страничной организации линейный адрес совпадает с физическим.
  • Логический адрес (виртуальный) — адрес‚ которым оперирует программа. Состоит из двух частей: селектор сегмента и смещения (offset).

Формула линейного адреса (в защищенном режиме x86):

Линейный_адрес = База_сегмента(Селектор) + Смещение

Режимы адресации процессора (x86 пример)

Способ вычисления адреса зависит от режима работы процессора:

  1. Реальный режим (Real Mode): Адрес = Сегмент * 16 + Смещение (например‚ 0x0400:0x0010 → 0x04010). Именно здесь «программа начинается в 0000:0000‚ а переменные — с 0040:0000».
  2. Защищенный режим (Protected Mode): Сегмент — это индекс в таблице дескрипторов (GDT/LDT)‚ где хранится база сегмента‚ лимит и права доступа.
  3. Длинный режим (Long Mode / x64): Сегментация практически не используется (база CS/DS/ES/SS = 0). Адрес = Смещение (RIP-relative addressing).

Как узнать адрес переменной/объекта в языке Си (Указатели)

В языке Си «частью» обычно является переменная‚ элемент массива‚ поле структуры или функция. Работа с адресами строится на указателях.

Основные операторы

Оператор Название Описание
& Оператор взятия адреса (Address-of) Возвращает адрес операнда (переменной). Результат имеет тип «указатель на тип операнда».
* Оператор разыменования (Dereference) Получает значение по адресу‚ хранящемуся в указателе.

Примеры кода

#include <stdio.h>
#include <stdint.h> // для uintptr_t

int main {
 int var = 42; // "Часть" данных в памяти
 int ptr = &var; // 1. Узнаем адрес части (переменной)
 
 printf("Адрес var: %p

"‚ (void)&var);
 printf("Значение ptr: %p
"‚ (void)ptr);
 printf("Значение по адресу: %d
"‚ ptr); // Разыменование
 
 // Адресная арифметика (указатель на отдельный объект ведет себя как массив из 1 элемента)
 // ptr + 1 указывает на "следующий" int за var (ОПАСНО: UB‚ если там нет объекта)
 
 // Получение адреса поля структуры (части структуры)
 struct { int x; int y; } s = {1‚ 2};
 int field_addr = &s.y; // Адрес части 'y'
 printf("Смещение y от начала структуры: %zu байт
"‚ (char)&s.y ⎯ (char)&s);
 
 return 0;
}

Важные нюансы

  • Тип указателя: При объявлении int p; звездочка относится к имени переменной‚ а не к типу. Тип — «указатель на int».
  • Void-указатель (void): Универсальный адрес без типа. Нельзя разыменовать и делать арифметику без приведения к конкретному типу.
  • Формат вывода: Используйте спецификатор %p и приведение к (void) для переносимости.
  • Адресная арифметика: Прибавление N к указателю сдвигает его на N * sizeof(Тип) байт.

Адресация в Ассемблере: режимы и инструкции

На уровне машинных кодов «узнать адрес» означает вычислить эффективный адрес (Effective Address — EA) и загрузить его в регистр или использовать в команде.

Основные режимы адресации (x86/x64)

  1. Непосредственная (Immediate): Операнд — сама константа. Адреса нет.
    mov eax‚ 1 ; Значение 1 в EAX
  2. Прямая абсолютная (Direct Absolute): Адрес задан явно в инструкции.
    mov eax‚ [0x00401000] ; Чтение из фиксированного адреса
  3. Регистровая (Register): Операнд в регистре.
    mov ebx‚ eax ; Копирование регистра
  4. Косвенная регистровая (Register Indirect): Регистр содержит адрес.
    mov eax‚ [ebx] ; Чтение по адресу в EBX
  5. Базисно-индексная (Base + Index + Scale + Displacement): Мощнейший режим для массивов/структур.
    mov eax‚ [rbx + rcx*4 + 10h] ; База RBX‚ индекс RCX‚ масштаб 4 (sizeof int)‚ смещение 16
  6. Адресация относительно RIP (RIP-relative addressing), стандарт x64:

    Адрес вычисляется как Адрес_следующей_инструкции + Смещение_в_команде. Это основа позиционно-независимого кода (PIC).

    lea rax‚ [rel my_var] ; Загрузка эффективного адреса my_var (RIP-relative)
    my_var dd 123

Инструкция LEA (Load Effective Address)

Это единственная инструкция‚ которая вычисляет адрес‚ но не обращается к памяти. Идеальна для получения адреса части структуры или локальной переменной в стеке.

; Пример: получение адреса поля структуры
; RDI = указатель на структуру
; Структура: struct { char a; int b; } -> смещение b = 4
lea rax‚ [rdi + 4] ; RAX = адрес поля b
; Аналог в Си: &ptr->b

Адрес части в контексте загрузки программы (Сегменты и Секции)

Когда программа загружается в память‚ ее «части» (код‚ данные‚ BSS‚ стек‚ куча) размещаются по конкретным адресам.

Сегменты памяти процесса (Linux/ELF‚ Windows/PE)

  • .text (CODE): Исполняемый код. Обычно Read+Execute. Адрес точки входа (Entry Point) известен из заголовка файла.
  • .data: Инициализированные глобальные/статические переменные. Read+Write.
  • .bss: Неинициализированные глобальные переменные. Занимает место в памяти‚ но не в файле.
  • Heap (Куча): Динамическая память (malloc‚ new). Адреса возвращаются функциями аллокатора.
  • Stack (Стек): Локальные переменные‚ адреса возврата. Растет вниз (на x86/x64). Адрес вершины в регистре RSP/ESP.

ASLR (Address Space Layout Randomization)

Современные ОС рандомизируют базовые адреса стека‚ кучи‚ библиотек и часто основного исполняемого файла (PIE — Position Independent Executable). Поэтому «узнать адрес части» статически (по карте файла) нельзя — нужно определять динамически во время выполнения.

Инструменты анализа адресов

  • Статический анализ: objdump -t‚ nm‚ readelf -S (Linux)‚ dumpbin /headers (Windows) — показывают смещения (Offsets/VMA) в файле.
  • Динамический анализ (Отладчики): GDB (info address var‚ p &var)‚ x/10x $rsp (просмотр стека)‚ info proc mappings (карта памяти).
  • /proc//maps (Linux): Файл‚ показывающий текущие диапазоны виртуальной памяти процесса с правами доступа и путями к файлам.

Адрес части в сетевом программировании (URI / URL)

Если «часть» — это ресурс в сети‚ адресом является URI (Uniform Resource Identifier). В языке Си работа с ними осуществляется через структуры addrinfo‚ sockaddr и функции getaddrinfo.

Структура URI

схема://[пользователь:пароль@]хост[:порт]/путь[?запрос][#фрагмент]

  • Хост (Host): Доменное имя или IP-адрес (IPv4/IPv6). Это «адрес машины».
  • Порт (Port): «Адрес процесса» на машине (16-битное число).
  • Путь (Path): «Адрес ресурса» внутри приложения (часть URI).

Получение сетевого адреса в Си (getaddrinfo)

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>

void resolve_host(const char hostname‚ const char service) {
 struct addrinfo hints‚ res‚ p;
 int status;
 char ipstr[INET6_ADDRSTRLEN];

 memset(&hints‚ 0‚ sizeof hints);
 hints.ai_family = AF_UNSPEC; // IPv4 или IPv6
 hints.ai_socktype = SOCK_STREAM; // TCP

 if ((status = getaddrinfo(hostname‚ service‚ &hints‚ &res)) != 0) {
 fprintf(stderr‚ "getaddrinfo: %s
"‚ gai_strerror(status));
 return;
 }

 printf("IP-адреса для %s:
"‚ hostname);
 for (p = res; p != NULL; p = p->ai_next) {
 void addr;
 char ipver;
 // Приводим к конкретной структуре sockaddr_in или sockaddr_in6
 if (p->ai_family == AF_INET) { // IPv4
 struct sockaddr_in ipv4 = (struct sockaddr_in )p->ai_addr;
 addr = &(ipv4->sin_addr);
 ipver = "IPv4";
 } else { // IPv6
 struct sockaddr_in6 ipv6 = (struct sockaddr_in6 )p->ai_addr;
 addr = &(ipv6->sin6_addr);
 ipver = "IPv6";
 }
 inet_ntop(p->ai_family‚ addr‚ ipstr‚ sizeof ipstr);
 printf(" %s: %s
"‚ ipver‚ ipstr);
 } freeaddrinfo(res); // Важно освободить!
}

Резюме: чек-лист «Как узнать адрес части»

Контекст Что такое «Часть» Как узнать адрес Ключевые инструменты/операторы
Язык Си (Runtime) Переменная‚ элемент массива‚ поле структуры Оператор & &var‚ &arr[i]‚ &struct->field
Ассемблер (x86/x64) Операнд в памяти (переменная‚ ячейка массива) Вычисление Эффективного Адреса (EA) LEA‚ режимы адресации ([base+index*scale+disp])‚ RIP-relative
Отладка / Реверс-инжиниринг Функция‚ глобальная переменная‚ секция Анализ таблиц символов / Карта памяти GDB (info address)‚ IDA/Ghidra‚ /proc/pid/maps‚ nm‚ objdump
Сетевое программирование Сервис на удаленном хосте DNS-резолвинг + Порт getaddrinfo‚ inet_ntop‚ структуры sockaddr
Архитектура ОЗУ (Hardware) Физическая ячейка памяти Трансляция Вирт. -> Физ. (MMU/Page Tables) CR3 (PGD)‚ Page Tables‚ TLB. Программист обычно не управляет этим напрямую.

«Узнать адрес части» — задача‚ решение которой лежит в плоскости уровня абстракции:

  1. На уровне исходного кода (C/C++), используйте оператор & и понимание адресной арифметики.
  2. На уровне машинного кода (Asm) — используйте инструкцию LEA и режимы адресации (особенно RIP-relative в x64).
  3. На уровне процесса/ОС — учитывайте ASLR‚ виртуальную память и используйте отладчики для динамического определения.
  4. На уровне сети — используйте getaddrinfo для преобразования имен в числовые адреса сокетов.

Глубокое понимание того‚ как сегменты‚ смещения‚ указатели и таблицы страниц работают вместе‚ является признаком квалифицированного системного программиста и реверс-инженера.