Указатели в языке Си?
почему программа завершилась? Вмешалась ОС? Но если так, то почему?
Потому что у тебя был неинициализированный указатель, который по стечению обстоятельств указывал в область памяти которую можно читать (мы это знаем, поскольку ты смог оттуда прочитать значение) но нельзя изменять (поскольку ну ты понял).

Почему я не могу поменять значение самого указателя (ведь указатель - это всего лишь ссылка на адрес памяти, а не на значение).
Значение указателя ты поменять можешь, но это не то, что было написано в коде. В коде было написано поменять значение на которое указатель указывает. Поменять указатель было бы a = (int *)10;

могу ли я указателю дать ссылку на конкретную ячейку в памяти?
Можешь, но под ОС с виртуальной памятью это в большинстве случаев не имеет смысла. API в таких ОС обычно устроены так, что ты просишь выделить область памяти с заданными характеристиками, а ОС выполняет выделение и возвращает тебе адрес выделенного участка.

получаю ошибку, ведь вряд ли адрес памяти у указателя хранится в виде 16-ричного целочисленного литерала.
В памяти всё хранится как последовательность байтов. Любую последовательность байтов можно проинтерпретировать как указатель. Но чтобы компилятор С понял что ты имеешь в виду, ему надо явно сказать, что ты хочешь проинтерпретировать целое число как адрес: int *a = (int *)0x0061FF18;
Вмешалась ОС? Но если так, то почему?
Потому что классическая ошибка - использование неинициализированного указателя. Ты обьявил a указателем на целое, но не присвоил ему никакого значения (через malloc(), calloc() или функции, возвращающие указатель). Обьявленная переменная была выделена в куче и получила какое-то мусорное значение, доступное твоей программе (вполне может быть указатель на часть кода, например, на ядро, на общие функции). Ты его прочитал.
При попытке записать по данному адресу поймал SIGSEGV, потому что данный адрес твоей программе по записи недоступен. Все, как должно быть.
могу ли я указателю дать ссылку на конкретную ячейку в памяти?
Теоретически можешь, но практически ось тебе сразу выдаст SIGSEGV, потому что писать ты можешь только в те области памяти, которые тебе доступны по записи.
warning: initialization of 'int *' from 'int' makes pointer from integer without a cast
Ну правильно говорит - инициализация делается без приведения типа. Такой финт ушами часто делают со строками - по умолчанию выставляя там указатель на NULL (что обычно означает, что строке еще не выделялась память), напрмер
char *str = (char *) NULL;