Argc and argv in early Research Unix
Recently I was peripherally involved in a Fediverse discussion about
(C's) argc and argv (the arguments to your main(), the start of a C
program). Famously, argv[] is an array of pointers to your program's
arguments (including the nominal name of the program),
and it's sort of traditional to terminate it with a NULL pointer
(although this isn't required by the Single Unix Standard; its
execve()
specification is silent on this). If you think about it, having both
argc and a NULL-terminated argv is redundant, since you could
determine one from the other. So me being me, I wondered how far back
argc and argv went in Unix (and if argv was NULL terminated from the
beginning). The answer turns out to be that they go all the way back
to Research Unix V1, which is before C existed, and argv[] wasn't
originally NULL terminated.
Update: Tony Finch pointed out that POSIX actually does specifically require argv[] to be NULL terminated (and the NULL not be counted in argc). See the comment for details.
The V1 exec(2) manual
page is specific about both sides of the V1 exec() API (which is
expressed in assembly language terms, since C wasn't invented yet).
Exec() is called with a NULL-terminated array of pointers to the
(zero-terminated) argument strings, but the invoked program receives
an explicit count of the arguments along with an array of argument
pointers, and the array is not listed as NULL-terminated. The V1
kernel source code for sysexec (in u2.s) doesn't appear
to put in a final NULL pointer or any other pointer value after the
regular argv[] pointers, so your program has to use argc to know when
to stop.
The logic of this split between the exec() API and the API to
programs is a bit clearer in the C code of the V4 exec() in
sys/ken/sys1.c.
Exec() needs to count the number of arguments in order to do things
like allocate the correct size of argv[] array on the stack of the new
program, and having created that count it might as well pass that to
the new program as argc. However, if I'm reading the V4 exec()
correctly, it adds a final '-1' right after the normal end of the
argv[] array:
while(na--) {
suword(ap=+2, c);
do
subyte(c++, *cp);
while(*cp++);
}
suword(ap+2, -1);
This trailing -1 remains present all the way through the V6 exec()
in sys/ken/sys1.c
(and I don't know why it was -1 instead of 0; the V6 crt0.s
doesn't seem to make any visible check for it).
Finally, in the V7 exece() in sys/sys.1, we
get an actual NULL pointer at the end of argv[]. However, this is less
of a terminator and more of a separator, because the addition of
environment variables in V7 has turned
argv[] into two arrays of pointers stacked on top of each other, one
for the arguments and one for the user environment (which is also
terminated with a NULL, because otherwise there's no way to tell).
Based on how the C program startup libc/csu/crt0.s
has a loop, I think that it finds the environment by walking the
argv[] array to find the separator NULL, although the kernel is still
providing argc as well as the argv[] array.
As far as I can tell, both System III and 4.2 BSD continue to add the separator NULL (it's more obvious in the 4.x BSD source, where there's an explicit copy of '0' into the user stack; in System III, it appears that the user stack section is pre-zeroed so the code just bumps the offset). BSD continued doing this at least as late as 4.3 BSD Reno (cf). Based on this repository, it appears that System V Release 2 for the Vax also separated argv[] and the environment with a NULL (cf vax/os/exec.c).
If there were Unix systems that later changed this to not have a separating NULL between argv[] and the environment (and thus not giving argv[] a terminating NULL), I don't know what they are. Instead, I suspect that either some C compilers on early non-Unix systems omitted the NULL at the end of their (made up) argv or that the ANSI C and POSIX people didn't want to explicitly require it.
Update: See above, POSIX does explicitly require NULL termination.
(Now you know why I was looking at exec() in early Unix and came
to understand its argv size limit.)