Breath of a New Process
There is no run-a-program syscall. There is fork, exec, and wait.
What you're wiring up
Every process on Unix is born by copying an existing one. fork() clones your shell, giving you two nearly identical processes that differ only in what fork returned. exec() then replaces the clone's entire program image with a new one, keeping the process ID and the open file descriptors. waitpid() is the parent standing at the door until the child is finished. That trio is process creation, in full.
An exit code is not a convention your program invents. It is one byte the kernel hands to the parent when a child dies, packed into an integer alongside signal information. You never read that integer directly; you ask it questions through macros, starting with WIFEXITED and WEXITSTATUS.
Two numbers every shell user has seen come from this stage. 127 means the shell searched $PATH and never found the program. 126 means it found the file and was not allowed to execute it. Both are decided by inspecting errno after exec fails, which is the only way you learn exec failed at all.
Assembly steps
hint
If execvp returns at all, it failed. Print the error and call _exit(127), not exit(), so the child does not double-flush inherited stdio buffers.
hint
The raw integer from waitpid is a packed struct in disguise. Never store or compare it directly.
hint
Expand after quote handling, so '$?' stays literal while "$?" expands.
hint
Check errno immediately after execvp: ENOENT versus EACCES.
Go deeper (after it passes)
Status codes as a public API, and errno discipline instead of exceptions, are exactly the case study in Clean Code M5 — Errors Without the Clutter. Read your platform's fork(2) and wait(2) man pages once end to end; they are shorter and better written than most tutorials.