Как найти адрес элемента памяти

Получить адрес переменной

int var;
&var; // возвращает адрес переменной var

Идентификатор должен быть переменной. Префиксный символ амперсанда (&), используемый совместно с именем переменной, возвращает адрес в памяти, где хранится значение этой переменной, в нашем случае, — для переменной var.

Получить адрес массива

array;

Чтобы получить адрес массива, мы просто используем имя массива, которое само по себе является указателем на адрес в памяти, в котором содержится первый элемент массива. Обратите внимание, что при использовании имени массива, С++ специфицирует его как указатель.

Получить адрес функции

int func(); 
int (*funcPtr)() = &func;

Чтобы получить адрес памяти для функции, просто используйте её имя без скобок с префиксным символом амперсанда, таким образом, компилятор будет знать, что вы  извлекаете адрес функции. Обратите внимание, что адрес присваивается указателю на функцию, то есть через этот указатель можно вызывать функцию, как мы делаем  это обычно, только использовать имя funcPtr, — это псевдоним функции func.

Получить адрес памяти и сохранить его в указателе

char *ptr = new char;
ptr; // вернуть адрес только что выделенной памяти

Указатель по умолчанию возвращает адрес памяти переменной, на которую он указывает. А в этом случае нет никакой переменной, мы просто связали ячейку памяти с указателем.

Разыменование указателей

int var = 4;
int *ptr = &var
*ptr; // возвращаемый результат - значение 4

Символ звездочки разыменовывает указатель, строка 3, таким образом, значение этого выражения является значение переменной, на адрес памяти которой ссылается указатель. ptr.

Теги: Си указатели. Указатель на указатель. Тип указателя. Арифметика указателей. Сравнение указателей.

Указатели

Это, пожалуй, самая сложная и самая важная тема во всём курсе. Без понимания указателей дальнейшее изучении си будет бессмысленным.
Указатели – очень простая концепция, очень логичная, но требующая внимания к деталям.

Определение

Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип.
Синтаксис объявления указателей

<тип> *<имя>;

Например

float *a;
long long *b;

Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.

#include <conio.h>
#include <stdio.h>
  
void main() {
	int A = 100;
	int *p;

	//Получаем адрес переменной A
	p = &A;

	//Выводим адрес переменной A
	printf("%pn", p);

	//Выводим содержимое переменной A
	printf("%dn", *p);

	//Меняем содержимое переменной A
	*p = 200;

	printf("%dn", A);
	printf("%d", *p);

    getch();
}

Рассмотрим код внимательно, ещё раз

int A = 100;

Была объявлена переменная с именем A. Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.

int *p;

Создали указатель типа int.

p = &A;

Теперь переменная p хранит адрес переменной A. Используя оператор * мы получаем доступ до содержимого переменной A.

Чтобы изменить содержимое, пишем

*p = 200;

После этого значение A также изменено, так как она указывает на ту же область памяти.
Ничего сложного.

Теперь другой важный пример

#include <conio.h>
#include <stdio.h>
  
void main() {
	int A = 100;
	int *a = &A;
	double B = 2.3;
	double *b = &B;

	printf("%dn", sizeof(A));
	printf("%dn", sizeof(a));
	printf("%dn", sizeof(B));
	printf("%dn", sizeof(b));

	getch();
}

Будет выведено

4
4
8
4

Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они
должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t),
это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве
случаев разницы между ними нет. Зачем тогда указателю нужен тип?

Арифметика указателей

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

Во-вторых, указатели поддерживают арифметические операции. Для их выполнения необходимо знать размер.

операция + N сдвигает указатель вперёд на N*sizeof(тип) байт.
Например, если указатель
int *p;
хранит адрес CC02, то после
p += 10;
он будет хранить адрес
СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате).

Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.

#include <conio.h>
#include <stdio.h>
  
void main() {
	int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int *p;

	p = A;

	printf("%dn", *p);
	p++;
	printf("%dn", *p);
	p = p + 4;
	printf("%dn", *p);

	getch();
}

Заметьте, каким образом мы получили адрес первого элемента массива

p = A;

Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому

p = &A[0];

Получить адрес первого элемента и относительно него двигаться по массиву.

Кроме операторов + и — указатели поддерживают операции сравнения.
Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.

#include <conio.h>
#include <stdio.h>
  
void main() {
	int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int *a, *b;

	a = &A[0];
	b = &A[9];

	printf("&A[0] == %pn", a);
	printf("&A[9] == %pn", b);

	if (a < b) {
		printf("a < b");
	} else {
		printf("b < a");
	}

	getch();
}

Если же указатели равны, то они указывают на одну и ту же область памяти.

Указатель на указатель

Указатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому.
Указатель на указатель определяется как

<тип> **<имя>;

Очевидно, ничто не мешает создать и указатель на указатель на указатель, и указатель на указатель на указатель на указатель и так далее. Это нам понадобится при работе с двумерными и многомерными массивами. А вот простой пример, как можно работать с указателем на указатель.

#include <conio.h>
#include <stdio.h>
  
#define SIZE 10

void main() {
	int A;
	int B;
	int *p;
	int **pp;

	A = 10;
	B = 111;
	p = &A;
	pp = &p;

	printf("A = %dn", A);
	*p = 20;
	printf("A = %dn", A);
	*(*pp) = 30;	//здесь скобки можно не писать
	printf("A = %dn", A);

	*pp = &B;
	printf("B = %dn", *p);
	**pp = 333;
	printf("B = %d", B);

	getch();
}

Указатели и приведение типов

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

#include <conio.h>
#include <stdio.h>
  
#define SIZE 10

void main() {
	int A = 10;
	int *intPtr;
	char *charPtr;

	intPtr = &A;
	printf("%dn", *intPtr);
	printf("--------------------n");
	charPtr = (char*)intPtr;
	printf("%d ", *charPtr);
	charPtr++;
	printf("%d ", *charPtr);
	charPtr++;
	printf("%d ", *charPtr);
	charPtr++;
	printf("%d ", *charPtr);
	
	getch();
}

В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.

NULL pointer — нулевой указатель

Указатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот «мусор» вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак.
Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.

int *ptr = NULL;

По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false.
Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float).

Это значит, что в данном случае

int *ptr = NULL;
if (ptr == 0) {
...
}

вполне корректная операция, а в случае

int a = 0;
if (a == NULL) {
...
}

поведение не определено. То есть указатель можно сравнивать с нулём, или с NULL, но нельзя NULL сравнивать с переменной целого типа или типа с плавающей точкой.

#include <stdlib.h>
#include <stdio.h>
#include <conio.h>

void main() {
	int *a = NULL;
	unsigned length, i;
	
	printf("Enter length of array: ");
	scanf("%d", &length);
	
	if (length > 0) {
		//При выделении памяти возвращается указатель.
		//Если память не была выделена, то возвращается NULL
		if ((a = (int*) malloc(length * sizeof(int))) != NULL) {
			for (i = 0; i < length; i++) {
				a[i] = i * i;
			}
		} else {
			printf("Error: can't allocate memory");
		}
	}

	//Если переменая была инициализирована, то очищаем её
	if (a != NULL) {
		free(a);
	}
	getch();
}

Примеры

Теперь несколько примеров работы с указателями
1. Пройдём по массиву и найдём все чётные элементы.

#include <conio.h>
#include <stdio.h>
  
void main() {
	int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	int even[10];
	int evenCounter = 0;
	int *iter, *end;

	//iter хранит адрес первого элемента массива
	//end хранит адрес следующего за последним "элемента" массива
	for (iter = A, end = &A[10]; iter < end; iter++) {
		if (*iter % 2 == 0) {
			even[evenCounter++] = *iter;
		}
	}

	//Выводим задом наперёд чётные числа
	for (--evenCounter; evenCounter >= 0; evenCounter--) {
		printf("%d ", even[evenCounter]);
	}

	getch();
}

2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.

#include <conio.h>
#include <stdio.h>
  
#define SIZE 10

void main() {
	double unsorted[SIZE] = {1.0, 3.0, 2.0, 4.0, 5.0, 6.0, 8.0, 7.0, 9.0, 0.0};
	double *p[SIZE];
	double *tmp;
	char flag = 1;
	unsigned i;

	printf("unsorted arrayn");
	for (i = 0; i < SIZE; i++) {
		printf("%.2f ", unsorted[i]);
	}
	printf("n");

	//Сохраняем в массив p адреса элементов
	for (i = 0; i < SIZE; i++) {
		p[i] = &unsorted[i];
	}

	do {
		flag = 0;
		for (i = 1; i<SIZE; i++) {
			//Сравниваем СОДЕРЖИМОЕ
			if (*p[i] < *p[i-1]) {
				//обмениваем местами АДРЕСА
				tmp = p[i];
				p[i] = p[i-1];
				p[i-1] = tmp;
				flag = 1;
			}
		}
	} while(flag);

	printf("sorted array of pointersn");
	for (i = 0; i < SIZE; i++) {
		printf("%.2f ", *p[i]);
	}
	printf("n");

	printf("make sure that unsorted array wasn't modifiedn");
	for (i = 0; i < SIZE; i++) {
		printf("%.2f ", unsorted[i]);
	}

	getch();
}

3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.

#include <conio.h>
#include <conio.h>
#include <stdio.h>

void main() {
	int length;
	char *p1, *p2;
	char tmp;
	float a = 5.0f;
	float b = 3.0f;

	printf("a = %.3fn", a);
	printf("b = %.3fn", b);

	p1 = (char*) &a;
	p2 = (char*) &b;
	//Узнаём сколько байт перемещать
	length = sizeof(float);
	while (length--) {
		//Обмениваем местами содержимое переменных побайтно
		tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		//не забываем перемещаться вперёд
		p1++;
		p2++;
	}

	printf("a = %.3fn", a);
	printf("b = %.3fn", b);

	getch();
}

В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof), всё равно мы будет обменивать местами байты двух переменных.

4. Найдём длину строки, введённой пользователем, используя указатель

#include <conio.h>
#include <stdio.h>

void main() {
	char buffer[128];
	char *p;
	unsigned length = 0;

	scanf("%127s", buffer);
	p = buffer;
	while (*p != '') {
		p++;
		length++;
	}

	printf("length = %d", length);
	getch();
}

Обратите внимание на участок кода

while (*p != '') {
	p++;
	length++;
}

его можно переписать

while (*p != 0) {
	p++;
	length++;
}

или

while (*p) {
	p++;
	length++;
}

или, убрав инкремент в условие

while (*p++) {
	length++;
}

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email

Строки

  • PavelK

Как найти адрес в памяти процесса?

Я в этой теме нуб, так что прошу помидорами не кидать. Есть объект, значение в памяти которого мне надо найти. Вот только я понятия не имею, как искать адрес в памяти, если я не знаю какое число я вообще должен найти. Знаю, что в игре, в которой я ищу объект, его уже находили, но не рассказывают как :(


  • Вопрос задан

    более трёх лет назад

  • 1258 просмотров

Смотрите документацию по следующим WinAPI вызовам: OpenProcess, ReadProcessMemory, WriteProcessMemory, CloseHandle. Кроме того, еще есть такая штука, как ограничения на чтение и запись в память других процессов и возможно надо будет устанавливать эти дополнительные флаги или запрашивать права.

Пригласить эксперта

Вам наверное адрес объекта в памяти надо увидеть, а не «…объект, значение в памяти которого мне надо найти» или все же адрес свойства объекта? Но в любом случае — создайте указатель на объект или свойство и инициализируйте его тем, что вам нужно, затем выведите этот указатель на stdout («%p») и увидите адрес желаемого. Причем, независимо Win или Lin .. :)


  • Показать ещё
    Загружается…

27 мая 2023, в 17:14

1000 руб./за проект

27 мая 2023, в 16:56

6000 руб./за проект

27 мая 2023, в 16:54

20000 руб./за проект

Минуточку внимания

Assuming your platform is Windows, I find it convenient to inject your own DLL into the target process. From there, you could possibly do a heap walk and look for the value. After you’ve got it, send it back to your process via IPC (for instance, with Boost’s message_queue).

EDIT

Blood, as you requested, here’s a little code and a food for thought. The DLL itself is pretty simple, for example something like this:


#include <Windows.h>

/** You can use this one to examine the given memory blocks.
  * However, since you're inside another process, you cannot use
  * std::cout. But you'll get the idea (just an example). The code
  * is from my another project.
  */
void MyDump(const void *m, unsigned int n)
{
        const unsigned char *p = reinterpret_cast<const unsigned char *>(m);
        char buffer[16];
        unsigned int mod = 1;

        memset(&buffer, 0, sizeof(buffer));

        std::cout << "------------------------------------------------------------------------------------nOffset     | Hex                                                | ASCII            |n------------------------------------------------------------------------------------n0x" << std::setfill('0') << std::setw(8) << std::hex << (long)m << " |";

        for (unsigned int i = 0; i < n; ++i, ++mod) {
                buffer[i % 16] = p[i];

                --mod;

                if (mod % 4 == 0)
                        std::cout << " ";

                ++mod;

                std::cout << std::setw(2) << std::hex << static_cast<unsigned int>(p[i]) << " ";

                if ((mod == 16 && i != 0) || i == n - 1) {
                        if (i == n - 1) {
                                for (unsigned int j = 0; j < (16 - mod) * 3; ++j)
                                        std::cout << " ";

                                if (mod <= 4)
                                        std::cout << " ";

                                if (mod <= 8)
                                        std::cout << " ";

                                if (mod <= 12)
                                        std::cout << " ";
                        }

                        mod = 0;

                        std::cout << "| ";

                        for (unsigned short j = 0; j < 16; ++j) {
                                switch (buffer[j]) {
                                        case 0x7:
                                        case 0x8:
                                        case 0x9:
                                        case 0xa:
                                        case 0xb:
                                        case 0xd:
                                        case 0xe:
                                        case 0xf:
                                                std::cout << " ";

                                                break;

                                        default: std::cout << buffer[j];
                                }
                        }

                        std::cout << " |";

                        if (i == n - 1) {
                                std::cout << "n------------------------------------------------------------------------------------n";

                                return;
                        }

                        memset(&buffer, 0, sizeof(buffer));

                        std::cout << "n0x" << std::setfill('0') << std::setw(8) << std::hex << (long)m + i << " |";
                }
        }
}

BOOL APIENTRY DllMain(HANDLE h_module, DWORD ul_reason_for_call, LPVOID)
{
        switch (ul_reason_for_call) {
                case DLL_PROCESS_ATTACH:
                        /** Do the heap walk here, please see
                          * http://msdn.microsoft.com/en-us/library/ee175819%28v=VS.85%29.aspx
                          * for enumerating the heap.
                          */

                        break;

                case DLL_THREAD_ATTACH: break;

                case DLL_THREAD_DETACH: break;

                case DLL_PROCESS_DETACH:
        }
}

Now that you have the DLL, you still need to inject it to the desired process. This can be easily done with EasyHook API. Download the library and see an example of unmanaged hook.

Указатели

Что такое указатели

Последнее обновление: 03.01.2023

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

Определение указателя

Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *.

тип_данных* название_указателя;

Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.

Например, определим указатель на объект типа int:

int *p;

Пока указатель не ссылается ни на какой объект. Теперь присвоим ему адрес переменной:

int main(void)
{
    int x = 10;		// определяем переменную
    int *p;			// определяем указатель
    p = &x;			// указатель получает адрес переменной
    return 0;
}

Получение адреса данных

Указатель хранит адрес объекта в памяти компьютера. И для получения адреса к переменной применяется операция &.
Эта операция применяется только к таким объектам, которые хранятся в памяти компьютера, то есть к переменным и элементам массива.

Что важно, переменная x имеет тип int, и указатель, который указывает на ее адрес тоже имеет тип int. То есть должно быть соответствие по типу.

Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p:

#include <stdio.h>

int main(void)
{
	int x = 10;
	int *p;
	p = &x;
	printf("%p n", p);		// 0060FEA8
	return 0;
}

В моем случае машинный адрес переменной x — 0x0060FEA8. (Для адресов в памяти применяется шестнадцатеричная система.) Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.

То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x.

Указатели в языке Си

Так как переменная x представляет тип int,
то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом,
переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.

Указатели в Си

И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.

Стоит отметить, что при выводе адреса указателя функция printf() ожидает, что указатель будет представлять void*, то есть указатель на значение типа
void. Поэтому некоторые компиляторы при некоторых настройках могут при компиляции отображать предупреждения. И чтобы было все канонически правильно, то
переданный указатель нужно преобразовать в указатель типа void *:

printf("%p n", (void *)p);

Получение значения по адресу

Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется
операция * или операция разыменования (dereference operator). Результатом этой
операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:

#include <stdio.h>

int main(void)
{
	int x = 10;
	int *p;
	p = &x;
	printf("Address = %p n", (void*) p);
	printf("x = %d n", *p);
	return 0;
}

Консольный вывод:

Address = 0060FEA8
x = 10

Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:

int x = 10;
int *p  = &x;
int y = *p;		// присваиваем переменной y значение по адресу из указателя p
printf("x = %d n", y);	// 10

Здесь присваиваем переменной y значение по адресу из указателя p, то есть значение переменной x.

И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:

int x = 10;
int *p = &x;
*p = 45;
printf("x = %d n", x);	 // 45

Так как по адресу, на который указывает указатель, располагается переменная x, то соответственно ее значение изменится.

Создадим еще несколько указателей:

#include <stdio.h>

int main(void)
{
	char c = 'N';
	int d = 10;
	short s = 2;
	
	char *pc = &c;			// получаем адрес переменной с типа char
	int *pd = &d;			// получаем адрес переменной d типа int
	short *ps = &s;			// получаем адрес переменной s типа short
	
	printf("Variable c: address=%p t value=%c n", (void*) pc, *pc);
	printf("Variable d: address=%p t value=%d n", (void*) pd, *pd);
	printf("Variable s: address=%p t value=%hd n", (void*) ps, *ps);
	return 0;
}

В моем случае я получу следующий консольный вывод:

Variable c: address=0060FEA3	value=N
Variable d: address=0060FE9C	value=10
Variable s: address=0060FE9A	value=2

По адресам можно увидеть, что переменные часто расположены в памяти рядом, но не обязательно в том порядке, в котором они определены в тексте программы:

Работа с памятью в языке Си

Понравилась статья? Поделить с друзьями:

Не пропустите также:

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

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии