Соглашения о вызовах запоминаются лучше, когда их вычитывают из настоящего вывода компилятора, а не заучивают по таблице регистров. Хорошее упражнение это функция на C с тремя параметрами int, возвращающая их сумму, скомпилированная под RV32 один раз с -O0 и один раз с -O2.
На -O0 листинг шумный. Функция строит кадр, сохраняет a0, a1 и a2 в стек, загружает их обратно, складывает и оставляет результат в a0 перед ret. На -O2 всё сжимается до двух инструкций add и ret. Соглашение то же, листинг совсем другой, и в этом сравнении весь смысл. Аргументы в обоих случаях приходят в регистрах аргументов, результат в обоих случаях уходит в a0. Остальное было собственной бухгалтерией компилятора.
Дальше пусть функция вызовет другую функцию, и вопрос в том, почему теперь нужно сохранять ra. ret это просто jalr x0, 0(ra), а внутренний call перезаписывает ra.
Если пропустить неоптимизированный шаг, люди склонны думать, что возня со стеком входит в соглашение. Не входит. В чём нет уверенности, так это в порядке. Начинать с -O2 чище, но тогда установка кадра потом всплывает как сюрприз.