О неправомерном доступе к памяти через указатели (ссылка на статью)


Комментарии:

2024-02-13 veector

Если Вы посмотрите, какой ассемблерный код генерируется для ссылок и указателей при вызове функций, то будете удивлены: он одинаков в обоих случаях.
Например, если брать gcc x86 (я использовал mingw в составе code::blocks), то отличия начинаются в более сложных функциях. Дело в том, что в gcc принято передавать все параметры через стек, поэтому, ваши простые функции имеют одинаковый "прототип" на ассемблере и машинных кодах, т.к. gcc нечего "сокращать" используя логику ссылок, по сравнению с логикой указателей. Но, когда появляются более сложные конструкции, тогда применение ссылок начинает влиять на код.

Пример, в котором бинарный/ассемблерный коды функций F1() и F2() отличаются, также отличаются и их "прототипы" вызова в машинных кодах.
#include <iostream>
 
using namespace std;
 
typedef int& IntRef_t;
typedef int* IntPtr_t;
 
void F1(IntPtr_t* IntPtrPtr)
{
    int IntValue = ++(**IntPtrPtr);
    printf("F1 = %i\n", IntValue);
}
 
void F2(IntRef_t& IntRefRef)
{
    int IntValue = ++IntRefRef;
    printf("F2 = %i\n", IntValue);
}
 
int main()
{
    int D[2] = {0, 0};
    IntPtr_t IntPtr;
 
    IntPtr = &D[0];
    F1(&IntPtr);
    IntRef_t IntRef1 = D[1];
    F2(IntRef1);
 
    printf("D[0] = %i, D[1] = %i\n", D[0], D[1]);
    return 0;
}

2024-02-16 alextretyak

Поддерживаю автора сайта. Метод end(), который возвращает "фейковый" итератор (который запрещено разыменовывать) — это вообще какой-то бред. Проблема усугубляется тем, что по стандарту должно быть возможно через −−end() получить итератор на последний элемент контейнера. А если кто пробовал самостоятельно реализовать двусвязный список, тот знает, что в последнем узле списка в поле next_node удобно хранить просто нулевой указатель, который и является признаком окончания списка. Но если следовать стандарту C++, то end() не может возвращать просто нулевой итератор, т.к. к нему невозможно применить операцию ‘−−’. Из-за этого приходится извращаться с каким-нибудь sentinel node, который например в реализации от Microsoft выделяется динамически в конструкторе списка, что приводит к нехорошим последствиям: к примеру, массив из миллиона пустых std::list-ов потребует миллион вызовов оператора new для выделения памяти под sentinel nodes.
[Хотя, насчёт невозможности применить операцию ‘−−’ к нулевому итератору я несколько преувеличил: в итераторе можно хранить не только указатель на узел, но ещё и указатель на сам объект-список. Но всё равно получается излишнее усложнение реализации для соответствия принятому стандарту.]

И даже сама терминология итераторов в C++ абсолютно бестолковая. О чём красноречиво говорит такой факт, что итераторов в стиле C++ больше нет ни в одном языке программирования.
Мне нравится, как итераторы реализованы в Python и почти также в Rust:
У контейнеров есть метод iter(), который возвращает итератор по этому контейнеру. У итератора есть метод next(), который выполняет переход к следующему элементу итерируемого контейнера и как-то сигнализирует о том, что элементов в контейнере больше нет (т.е. текущий элемент был последний) — в Python это делается через raise StopIteration, в Rust через возврат None.
На этом слайде презентации есть сравнительная таблица итераторов в C++, D, C#, Rust, Python и Java. Как видно из этой таблицы, лишней сущности last [точнее after_last, он же end] нет ни в D, ни в C#, ни в Rust, ни в Java, ни в Python. И только C++ идёт своим особым путём, трактуя понятие "итератор" как навороченный указатель. Во всех нормальных остальных языках программирования под итератором понимается сущность, которая обеспечивает возможность обхода контейнера или какого-либо итерируемого объекта.

veector
Пример, в котором бинарный/ассемблерный коды функций F1() и F2() отличаются, также отличаются и их "прототипы" вызова в машинных кодах.
Ваш пример некорректен.
Функция F2 у вас на самом деле принимает обычную одиночную ссылку на int. Т.к. в данном случае имеет место т.н. "reference collapsing", а именно срабатывает правило ‘T& & → T&’. В этом легко убедиться, если попытаться добавить перегруженную функцию void F2(IntRef_t IntRef) — в этом случае компилятор выдаст ошибку "C2084: function 'void F2(IntRef_t)' already has a body". (Дело в том, что в C++ нет такого понятия, как ссылка на ссылку, поэтому то в C++11 для обозначения нового типа ссылок [rvalue references] ввели запись &&, т.к. в С++03 она не имела смысла, и ваш пример, кстати, не скомпилируется с опцией -std=c++03 — https://godbolt.org/z/c9ddPsqz3, т.к. "reference collapsing" появился только в C++11.)
А функция F1 принимает двойной указатель (т.е. указатель на указатель на int). Если это исправить, то генерируемый ассемблерный/машинный код для F1 и F2 будет полностью совпадать (за исключением адреса строкового литерала "F2 = %i\n").

Автор сайта
А Си накладывает ограничения: нельзя с помощью goto перейти на переменную или запрыгнуть внутрь цикла. Выпрыгнуть из цикла — пожалуйста, а внутрь — ни-ни.
Вообще-то, в Си можно запрыгнуть внутрь цикла. :)(: Вот пример: https://godbolt.org/z/4xqeM7Kxq.
И даже в C++ можно. При условии, что goto не пропускает объявления переменных. (Но, разумеется, делать так не следует — оба приведённых мной примера являются UB и компилятор выдаёт предупреждение об использовании неинициализированной переменной.)

2024-02-16 veector

alextretyak, все так. Я выбрал тот стандарт и то свойство, которые были удобны для демонстрации.

Я не знаком с Rust, но у меня есть к вам вопрос по теме итераторов.
У контейнеров есть метод iter(), который возвращает итератор по этому контейнеру. У итератора есть метод next(), который выполняет переход к следующему элементу итерируемого контейнера и как-то сигнализирует о том, что элементов в контейнере больше нет (т.е. текущий элемент был последний)
Зачастую, нужно несколько одновременно работающих итераторов с одной таблицей. Из-за этого, приходится делать отдельные объекты типа итератор и создавать их столько раз, сколько нужно. Вот этот метод iter() у контейнеров, он позволяет сделать несколько разных итераторов, работающих независимо, или это посто один итератор встроенный в класс контейнера?

2024-02-16 Автор сайта

veector
целых три, на мой взгляд, важных нюанса: Шаренная память ... звуковые карты, видео адаптеры ...
Нет никакого смысла инициализировать данные кучи или программного стека
Первые два нюанса действительно особый случай. Настолько особый, что и дисциплина обращений к такой памяти особая. Чтение такой памяти привносит недетерминизм, то есть непредопределённость, по-русски говоря. А запись в такую память имеет побочные эффекты. Соответственно функции, читающие из такой памяти, обладают недетерминизмом, а записывающие обладают побочным эффектом.

А вот с третьим перечисленным Вами нюансом я не согласен. В чём смысл вывода типов? Фишка в том, что переменная не создаётся то тех пор, пока в неё что-то не записали. Ведь это правильно — читать только тогда, когда в неё что-то записали. А запись — это вычисление значение этой переменной — до того, как она понадобилась для чтения. Поэтому неважно, где такая переменная находится: в статической ли памяти, в стеке или куче. Важно, что чтению должна предшествовать запись, а если первым всё-таки идёт чтение, то имеем непредсказуемое значение.

А если переменная не записывалась и не читалась, то она просто не нужна. Компилятор языка, где нет вывода типов, должен потрудиться, чтобы выявить такие переменные и выдать предупреждение. А вывод типов просто на автомате не создаёт таких переменных. Компилятор упрощается: ему не надо искать неиспользуемые переменные.

Третий Ваш нюанс имеет совсем другой нюанс. Если ОС выделяет программе физическую память, как она есть, то чтение неинициализированной памяти помогает хакерам узнавать всякие пароли, ключи шифрования и прочие чувствительные вещи. Поэтому современные ОС выделяемую память прописывают чем-нибудь, чаще нулями. Это замедляет работу, но повышает безопасность.

alextretyak
Вообще-то, в Си можно запрыгнуть внутрь цикла.
Надо попробовать. А то прочитал когда-то, принял это для себя как аксиому. А может, аксиома касалась не Си, а другого языка? Запросто могло так быть.

2024-02-17 alextretyak

veector
т.к. универсальных языков нет.
Я бы уточнил: «на данный момент/пока ещё нет». Но это не означает, что такой язык невозможно создать в принципе.
И работы в этом направлении активно ведутся: Mojo, daslang.org. (Это примеры из совсем новых языков, хотя, разумеется, попытки создания универсальных языков были и раньше.)
Собственно, и я тоже занимаюсь разработкой такого языка. (Вот моя последняя статья на эту тему: https://habr.com/ru/articles/784294/, хотя она больше про компилятор, но в конце есть немного и про язык.)
Все эти языки без сборщика мусора и поддерживают низкоуровневые указатели, поэтому вполне пригодны для системного программирования, обладая при этом синтаксисом скриптовых языков.

Инициализация — это же чисто человеческое понятие.
Значение, которым нужно проинициализировать какую-то переменную, зависит от решаемой человеком задачи.
А разве бывают решаемые человеком задачи, в которых было бы допустимо использование неинициализированных переменных?
Неинициализированными можно оставить только такие переменные, к которым вообще не происходит обращения, что случается довольно редко.

Автоматически компилятором вставлять код, что бы все переменные инитить согласно их типу, или заставить инитить человека, не допуская переменных без инициализации, ну, это такое себе, спорное решение.
Нормальное решение, которого придерживаются практически все современные языки программирования. И во многих реализована оптимизация удаления кода "излишних" инициализаций (например, в auto i = 0; i = 1; присваивание нулю будет отброшено {}).
К тому же, инициализация бывает разная.
Например, в таком коде:
int i;
if (flag)
    i = 1;
else
    i = 2;
printf("%i", i);
переменную i можно считать корректно проинициализированной. Суть в том, чтобы компилятор делал проверку того, что на всех возможных путях выполнения кода переменная инициализируется перед тем, как она используется.
Заметьте, что хотя C++ допускает неинициализированные переменные на уровне языка, некоторые компиляторы C++ (Clang, MSVC) выдают предупреждение об использовании потенциально неинициализированных переменных. Моя позиция (и насколько я понимаю позиция автора сайта) заключается в том, чтобы это предупреждение сделать ошибкой компиляции.

Ибо, например, реальное ОЗУ подставится в момент первого обращения к данному адресному пространству.
И что же будет находиться по этому адресному пространству? Какой-то неизвестный мусор? Память, заполненная какими-то данными от других процессов (в которой могут оказаться пароли, ключи шифрования и прочее)? Ничего подобного. Практически все операционные системы обеспечивают защиту от возможности получения мусорных страниц памяти от других процессов путём их зануления. Да, фактически такое зануление не выполняется сразу же. Можно запросить 1 гигабайт памяти и не опасаться того, что операционная система будет забивать этот гигабайт нулями немедленно. Но! Можно полагаться на тот факт, что при первом обращении к любой странице памяти из запрошенного гигабайта, содержимое этой страницы памяти будет гарантированно обнулено. Таким образом, хотя реально в выделенном блоке памяти находится (точнее будет находиться, т.к. физические страницы назначаются по мере обращения к виртуальным) какой-то мусор (возможно, с информацией из других процессов), но виртуально запрошенный блок памяти уже полностью проинициализирован нулями. И этим можно пользоваться. Вот пример такого использования:
https://fortran-lang.discourse.group/t/why-is-this-fortran-code-so-much-faster-than-its-c-counterpart/3746
(Там человек спрашивает, почему одинаковый по смыслу код в Фортране работает намного быстрее, чем в C++.)
Обратите внимание на строку allocate(x(n), source = 0d0).
Здесь производится выделение массива из 3 миллиардов вещественных чисел, проинициализированных нулями. И компилятор Фортрана догадывается использовать в данном случае calloc(), который в свою очередь полагается на поведение операционной системы и не делает явного зануления всех байт выделенного блока памяти.
Но компилятор C++ не может повторить такой трюк, т.к. контейнеры STL не обращаются к malloc() напрямую, а используют глобальный operator new, который может быть переопределён, а потому, полагаться на то, что полученная им память будет заполнена нулями, не получится.

Влияет и очень существенно.
Например, на скорость работы программы, что особенно заметно на большом объеме обрабатываемых данных (это не обязательно большие массивы, а может быть очень много маленьких).
С большими массивами, которые имеет смысл оставлять неинициализированными, программист имеет дело очень редко. В остальном же коде наличие неинициализированных переменных приводит (либо может привести) к ошибкам в работе программы, и от этих ошибок, в первую очередь, и должен защищать компилятор. А когда действительно нужен непроинициализированный массив, можно использовать специальные средства в языке программирования: в языке D это uninitializedArray, а также void initializers. В языке C++20 это std::make_unique_for_overwrite, а также предлагаемый string::resize_and_overwrite.

у меня есть к вам вопрос по теме итераторов.
Зачастую, нужно несколько одновременно работающих итераторов с одной таблицей.
Во-первых, я не очень понимаю, что вы имеете в виду под таблицей. Хеш-таблицу?
Просто "таблицы" есть, насколько мне известно, только в языке Lua и являются гибридом хэш-таблицы и массива.

Во-вторых, давайте попробуем устранить путаницу в терминологии, которую внёс C++. То, что в C++ принято называть итератором, в других языках называют чаще курсором [и работают с ним несколько по-другому]. А итератор лишь обеспечивает возможность обхода итерируемого объекта. Его можно получить напрямую у контейнера методом iter() (в этом случае будет производиться обход по всем элементам этого контейнера). Его [итератор] возвращают методы наподобие reversed() [позволяет обойти все элементы контейнера в обратном порядке], filter() [обходит только элементы, удовлетворяющие определённому условию] и т.п.

Вот этот метод iter() у контейнеров, он позволяет сделать несколько разных итераторов, работающих независимо, или это посто один итератор встроенный в класс контейнера?
Каждый вызов метода iter() создаёт новый независимый объект-итератор. В документации приводится пример "ручной" работы с итератором.
А вот пример получения двух независимых итераторов от одного контейнера и такой же "ручной" работы с ними:
fn main() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();
    let mut v2_iter = v1.iter();

    print!("{}", v1_iter.next().unwrap());
    print!("{}", v1_iter.next().unwrap());
    print!("{}", v2_iter.next().unwrap());
}
Но вообще-то, так итераторы обычно не используют. Методы iter() и next() нужны не для явного вызывания, а для неявного:
fn main() {
    let v1 = vec![1, 2, 3];

    for val in v1 { // методы iter() и next() вызываются компилятором неявно
        println!("Got: {}", val);
    }
}

2024-02-18 Автор сайта

Ваш код
int i;
if (flag)
    i = 1;
else
    i = 2;
printf("%i", i);
я бы написал по-другому. Вывод типов, а следовательно и объявление переменной, и её инициализация, может делаться и условным выражением, что лаконичнее и элегантнее (последнее зависит от вкусов).
а = (если б < в
    0
     иначе
    1.0)
или
а = (если б < в; 0; иначе 1.0)
И не только условным выражением, но и переключателем. При этом выбирается:
Если же в ветвях условного выражения встречаются несовместимые типы, то это ошибка.

Но! Можно полагаться на тот факт, что при первом обращении к любой странице памяти из запрошенного гигабайта, содержимое этой страницы памяти будет гарантированно обнулено.
В программе на Вашем языке, как и на моём, это действительно будет так. Но мир состоит не только из честных людей. Оставшиеся люди выберут другой язык, чтобы иметь доступ к неинициализированной памяти. Увы, но ОС вынуждена всё-таки обнулять память.

2024-02-19 veector

alextretyak
Спасибо за пояснения про итераторы в Rust, все понятно, вопросов нет, сделано хорошо.

Каждый вызов метода iter() создаёт новый независимый объект-итератор.
По поводу "инициализации", вы же сами и написали, что "К тому же, инициализация бывает разная. Например, в таком коде:" — как вы это будете "занулять" на автомате? Я уже не говорю про переменную flag, значение которой, по вашей с Автором сайта логики автоматической инициализации, тоже ведь кто-то когда-то должен быть записать.

И что же будет находиться по этому адресному пространству? Какой-то неизвестный мусор?
Как я уже замечал ранее, современная тенденция в разработке аппаратуры — проецировать в адресное пространство процессоров не только ОЗУ, а вообще все на свете. Например, там может быть ПЗУ со значением серийного номера процессора или MAC-адресом для встроенной сетевой карты по умолчанию. Применений очень много, и если вы с ними не сталкиваетесь, то не значит, что этого нет.

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

По поводу "автозануления" ОЗУ операционными системами, скажу так, что когда на кону стоит скорость обработки, то "автозануление" не используется. Ну и зависит это от много, включая от настроек ядер операционных систем, вот тут как раз об этом же и написали в самом конце: https://fortran-lang.discourse.group/t/why-is-this-fortran-code-so-much-faster-than-its-c-counterpart/3746/12
In this particular case, the system calloc implementation did better than C++'s operator new + memset. But as others have shown, you cannot generalize this to all platforms (OS's, architectures).

2024-02-20 alextretyak

Автор сайта
я бы написал по-другому. Вывод типов...
Да, вывод типов, а также выражение «если-иначе» — это всё хорошо, но я хотел сделать акцент на другом. А именно, на случаях очень сложных инициализаций.
Допустим нам необходимо посчитать число n, которое в дальнейшем будет использоваться как количество итераций некоторого цикла. Если написать так:
auto n = 0;
if (flag1) {
    if (flag2) {
        n = 1;
    }
    else {
        n = 2;
    }
    if (flag3)
        n++;
}
else {
    if (flag4) {
        n = 3;
    }
    else {
        ... // здесь забыли проинициализировать `n`
    }
}
// ... `n` используется где-то ниже
то в случае, когда выполнение кода пойдёт по пути, в котором n не инициализируется, в n окажется начальное значение 0, и при этом возможно возникновение очень трудно уловимой ошибки.

Если же вместо auto n = 0; написать int n; [я использую здесь синтаксис C++ просто чтоб было понятнее], то это будет предписывать компилятору, что n инициализируется где-то дальше, и компилятор должен проверить, что n действительно инициализируется перед использованием (ведь с записью auto n = 0; такую проверку компилятор делать не будет). Так программист может написать int n; в случае, когда ему необходимо проверить, что n инициализируется на всех путях выполнения. Язык с более продвинутым выводом типов может разрешить даже запись auto n; вместо int n;.

В программе на Вашем языке, как и на моём, это действительно будет так. Но мир состоит не только из честных людей. Оставшиеся люди выберут другой язык, чтобы иметь доступ к неинициализированной памяти.
Если честно, не очень понимаю, что вы имели в виду. Я говорил как раз о том, чтобы, наоборот, не занулять явно память полученную от системы, т.к. это повышает производительность программы.

Увы, но ОС вынуждена всё-таки обнулять память.
Верно. Поэтому какой бы язык программирования не выбрал программист (да хоть даже ассемблер), он не сможет получить "мусорные" данные от других процессов. При всём желании. Т.к. смотрим, что написано в документации наиболее распространённых операционных систем: Windows (если говорить про desktop) и Linux (если говорить про серверный сегмент).
В Windows любая аллокация памяти (malloc, HeapAlloc или самописный аллокатор памяти — неважно) в итоге приводит к системному вызову VirtualAlloc, в документации к которому написано просто и понятно:
Memory allocated by this function is automatically initialized to zero.
В Linux аналогично: в документации к mmap сказано:
MAP_ANONYMOUS
The mapping is not backed by any file; its contents are initialized to zero.
Да, есть ещё флаг MAP_UNINITIALIZED, но он требует включения специальной опции при сборке ядра и на практике используется только в мире embedded-устройств.

P.S. Да, разумеется, гарантированно зануляет память не каждая операционная система, но давайте будем честными: кто из нас писал программы под операционные системы, которые этого не делают? Я вот не писал, и, возможно, никогда и не буду. Так зачем мне об этом думать? Думать о том, что мой язык программирования будет неэффективно работать на системах, под которые практически никто не пишет. Пусть те, кто под них пишет, выбирают другой язык программирования (если для них это настолько критично), благо выбрать есть из чего.
Причём заметьте, я сказал «будет неэффективно работать», а не «вообще не будет работать» [на таких системах]. Ибо, когда из исходного кода компилятор генерирует исполняемый двоичный файл, это всегда производится для конкретной архитектуры процессора и для конкретной операционной системы. [Даже если взять платформы наподобие .NET Framework, где приложения распространяются на некоем промежуточном языке, то в конечном счёте для выполнения программы должен получиться машинный код под конкретную архитектуру и операционную систему.] Таким образом, компилятор в любом случае знает, будет ли полученная от системы память гарантированно занулена или не будет, и во втором случае просто добавит
memset(mem, 0, size)
и программа будет корректно работать, пусть и не так эффективно (особенно в случае выделения огромного блока памяти, который будет использоваться лишь частично).

2024-02-20 veector

... но давайте быть честными: кто из нас писал программы под операционные системы, которые этого не делают?
Например, я, причем это 99% моей непосредственной работы.

2024-02-20 Автор сайта

выражение «если-иначе» — это всё хорошо, но я хотел сделать акцент на другом. А именно, на случаях очень сложных инициализаций.
Думаю, при должной старательности сложные инициализации можно упростить. Или даже выделить инициализацию в отдельную функцию, ведь return не может возвратить неинициализированное значение. В примерах выше условный оператор был превращён в условное выражение, то есть конструкцию, возвращающую аргумент. Можно подумать над тем, чтобы и цикл что-то возвращал, тогда он будет не оператором цикла, а выражением цикла.

в n окажется начальное значение 0, и при этом возможно возникновение очень трудно уловимой ошибки.
А если нулём прописывается указатель, то вообще беда.

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

Если честно, не очень понимаю, что вы имели в виду. Я говорил как раз о том, чтобы, наоборот, не занулять явно память полученную от системы
Имел в виду не «занулять» (в том числе и бессмысленно, лишь бы чем-то проинициализировать), а присваивать осмысленное значение, вытекающее из логики алгоритма.

какой бы язык программирования не выбрал программист (да хоть даже ассемблер), он не сможет получить "мусорные" данные от других процессов. При всём желании.
Чтобы бороться с преступниками, надо думать как преступники. Вообразите, что вам надо украсть миллиард с банковского счёта. Вы и операционку пропатчите, чтобы она не обнуляла память при выделении, и язык себе выберете без гарантированной инициализации.

кто из нас писал программы под операционные системы, которые этого не делают?
Я писал! При этом я писал под Windows. По-моему (но могу ошибиться), Windows XP ещё не обнуляла память, а началось это с Vista. Иногда сталкивался с тем, что программа начинает вести себя по-другому просто от добавления ещё одной переменной. Тогда понимаешь, в чём дело, и ищешь: где это я забыл проинициализировать? Изменение поведения от того, что не может поменять поведение, как раз и было признаком невыполненной инициализации.

мой язык программирования будет неэффективно работать на системах, под которые практически никто не пишет.
ОС РВ — вполне себе достойная уважения ниша. Имеют право не обнулять память потому, что перечень установленных программ там заранее зафиксирован.

я сказал «будет неэффективно работать»
Неэффективность будет связана с двукратной записью в одни и те же участки памяти: сперва нуля, а потом собственно значения. Но не трёх и более кратной записью. Так что максимальное торможение — максимум в 2 раза. В реальности меньше, потому что есть ещё статическая память, операции регистр-регистр, есть кэш процессора и т.д. Кстати, в ОС жёсткого реального времени динамического выделения памяти нет. Память выделяется статически. Там эта тема не актуальна.

2024-02-22 alextretyak

veector
Например, я, причем это 99% моей непосредственной работы.
Было бы интересно увидеть больше конкретики. Что за операционная система(-ы), что за архитектура процессора, есть ли возможность посмотреть документацию онлайн...

Автор сайта
Чтобы бороться с преступниками, надо думать как преступники. Вообразите, что вам надо украсть миллиард с банковского счёта. Вы и операционку пропатчите, чтобы она не обнуляла память при выделении, и язык себе выберете без гарантированной инициализации.
Патчить ядро Windows для доступа к чужой памяти? Зачем ломать стену, когда рядом есть дверь? :)(:
Помнится, ещё в Windows 95 или 98 я использовал программу ArtMoney для получения бесконечных денег в играх. Когда узнал, как она работает, был весьма удивлён — штатными средствами WinAPI (а именно, функциями ReadProcessMemory/WriteProcessMemory) можно получить доступ к памяти любого другого процесса. Хотя, начиная с Vista эти функции требуют административных привелегий.

По-моему (но могу ошибиться), Windows XP ещё не обнуляла память, а началось это с Vista.
Обнуляла. Но речь только про системную функцию VirtualAlloc. Она обнуляла память всегда с момента своего появления в Windows NT, вот только в прикладных программах эта функция непосредственно, как правило, не используется, т.к. она выделяет только блоки размером кратным 64Кб. В программах на Си чаще всего используется библиотечная функция malloc(), а в C++ — оператор new, который стандартно реализован через вызов того же malloc(). А вот память, которую вернул malloc(), далеко не всегда занулена (собственно, поэтому есть отдельная функция — calloc(), которая возвращает занулённый блок памяти): во-первых, в Debug-сборках реализация malloc() перед тем как вернуть указатель заполняет каждый байт выделенного блока памяти значением 0xCD (как раз таки для обнаружения обращений к неинициализированным данным). И, во-вторых, malloc() может вернуть блок памяти, который был ранее освобождён функцией free(), и в этом блоке будут "мусорные" данные, вот только не от других процессов, а от этого же процесса, записанные им в свою память во время его предыдущей работы.

2024-02-22 veector

alextretyak, много всяких разных, наверное, всё, что придумается в embedded сфере, причем, всевозможных версий.
Более подробно, увы, не скажу.