Виртуальные адреса требуются для запуска нескольких программ на компьютере.
Предположим, что нет механизма виртуальных адресов. Компиляторы и редакторы ссылок генерируют макет памяти с заданным шаблоном. Инструкция (текстовый сегмент) размещается в памяти с адреса 0. Затем идут сегменты для инициализированных или неинициализированных данных (data и bss) и динамической памяти (куча и стек). (см., например, https://www.geeksforgeeks.org/memory-layout-of-c-program/, если у вас нет представления о расположении памяти)
Когда вы запустите эту программу, она будет занимать часть памяти, которая больше не будет доступна для других процессов совершенно непредсказуемым образом. Например, адреса от 0 до 1M будут заняты, или от 0 до 16k, или от 0 до 128M, это полностью зависит от характеристик программы.
Если теперь вы хотите одновременно запустить вторую программу, куда ее инструкции и данные попадут в память? Адреса памяти генерируются компилятором, который, очевидно, не знает во время компиляции, что будет свободной памятью. И помните, что адреса памяти (для инструкций или данных) как-то жестко запрограммированы в программном коде.
Вторая проблема возникает, когда вы хотите запустить много процессов и вам не хватает памяти. В таких ситуациях некоторые процессы выгружаются на диск и позже восстанавливаются. Но после восстановления процесс пойдет туда, где память свободна, и снова, это что-то непредсказуемое и потребует изменения внутренних адресов программы.
Виртуальная память упрощает все эти задачи. При запуске процесса (или восстановлении его после замены) система просматривает свободную память и заполняет таблицы страниц, чтобы создать сопоставление между виртуальными адресами (управляемыми процессором и всегда неизменными) и физическими адресами (что зависит от свободной памяти на компьютер в данный момент).