Понятие «адрес части» является фундаментальным в информатике‚ но его смысл кардинально меняется в зависимости от контекста: работаем ли мы с оперативной памятью (низкий уровень‚ указатели‚ ассемблер)‚ виртуальной памятью процесса (сегменты‚ смещения) или сетевыми ресурсами (URI/URL). В этой статье мы подробно разберем основные сценарии определения адресов‚ опираясь на архитектуру ЭВМ‚ языки Си/Ассемблер и сетевое программирование.
Адресация в памяти: базовые концепции
Прежде чем узнавать адрес‚ нужно понять‚ что он собой представляет. В архитектуре фон Неймана память — это линейный массив ячеек‚ каждая из которых имеет уникальный номер — адрес.
Физический‚ линейный и логический адреса
- Физический адрес — сигнал на адресной шине процессора‚ выбирающий конкретную ячейку ОЗУ.
- Линейный адрес — адрес в плоском адресном пространстве (в 32-битном режиме до 4 ГБ). В режиме реального адресации или при отключенной страничной организации линейный адрес совпадает с физическим.
- Логический адрес (виртуальный) — адрес‚ которым оперирует программа. Состоит из двух частей: селектор сегмента и смещения (offset).
Формула линейного адреса (в защищенном режиме x86):
Линейный_адрес = База_сегмента(Селектор) + Смещение
Режимы адресации процессора (x86 пример)
Способ вычисления адреса зависит от режима работы процессора:
- Реальный режим (Real Mode): Адрес =
Сегмент * 16 + Смещение(например‚0x0400:0x0010→0x04010). Именно здесь «программа начинается в 0000:0000‚ а переменные — с 0040:0000». - Защищенный режим (Protected Mode): Сегмент — это индекс в таблице дескрипторов (GDT/LDT)‚ где хранится база сегмента‚ лимит и права доступа.
- Длинный режим (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)
- Непосредственная (Immediate): Операнд — сама константа. Адреса нет.
mov eax‚ 1 ; Значение 1 в EAX - Прямая абсолютная (Direct Absolute): Адрес задан явно в инструкции.
mov eax‚ [0x00401000] ; Чтение из фиксированного адреса - Регистровая (Register): Операнд в регистре.
mov ebx‚ eax ; Копирование регистра - Косвенная регистровая (Register Indirect): Регистр содержит адрес.
mov eax‚ [ebx] ; Чтение по адресу в EBX - Базисно-индексная (Base + Index + Scale + Displacement): Мощнейший режим для массивов/структур.
mov eax‚ [rbx + rcx*4 + 10h] ; База RBX‚ индекс RCX‚ масштаб 4 (sizeof int)‚ смещение 16 - Адресация относительно 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. Программист обычно не управляет этим напрямую. |
«Узнать адрес части» — задача‚ решение которой лежит в плоскости уровня абстракции:
- На уровне исходного кода (C/C++), используйте оператор
&и понимание адресной арифметики. - На уровне машинного кода (Asm) — используйте инструкцию
LEAи режимы адресации (особенно RIP-relative в x64). - На уровне процесса/ОС — учитывайте ASLR‚ виртуальную память и используйте отладчики для динамического определения.
- На уровне сети — используйте
getaddrinfoдля преобразования имен в числовые адреса сокетов.
Глубокое понимание того‚ как сегменты‚ смещения‚ указатели и таблицы страниц работают вместе‚ является признаком квалифицированного системного программиста и реверс-инженера.