InFeeo
Language

The early Research Unix exec(2) argv size limit(tuhs.org)

×
Link preview The early Research Unix exec(2) argv size limit When I wrote up how V7 gave us environment variables, I mentioned that up to V6, exec(2) had a limit of 510 bytes of command line arguments (including argv[0], the nominal name of your program). You can see the check in the V6 kernel exec() code in sys/ken/sys1.c (where it returns E2BIG in this case). You might wonder where this limit comes from and why. When you exec() something, you discard your current process's memory and address space to create create a new one for the new program. Your current (user) memory includes the argv you're passing to exec(), so the kernel has to copy it from your user space into the kernel and then back, temporarily holding it in some sort of kernel memory. In a modern kernel you might dynamically allocate this kernel memory in exec() through the kernel equivalent of malloc(), but the Research Unix kernels were simple and didn't have that sort of thing. Instead, through Research Unix V6, they got their temporary scratch space for exec() by allocating a disk buffer, reusing a facility the kernel already needed. These disk buffers were 512 bytes long, which is more or less where the 510 byte limit on argument size comes from. (I don't know why it's 510 bytes instead of 512; I've been unable to follow the code closely enough to see if it slips in a use of the last two bytes of the buffer for something else.) You might innocently think that using a disk buffer just pushes the problem of dynamic allocation of (kernel) memory back one layer, to the disk buffer system. However, early Research Unix kernels are more brute force than that. The V6 kernel has a fixed (and limited) chunk of memory reserved for disk buffers, the buffers array in sys/dmr/bio.c, with its size set by NBUF in sys/param.h. The default NBUF isn't very large, but early Research Unix ran on small systems and had low limits in general (the same param.h sets a limit of 50 processes for the entire system). This straightforward approach to exec() and disk buffers goes back to at least Research Unix V4 (I haven't looked earlier than that). In V7, the kernel implementation is rather more complex because it needs to handle environment variables too, but it still sort of uses the disk buffer trick. In order to get the extra space without using much extra RAM, V7 uses swap space, writing to it and reading back from it through V7's general disk buffer system (which probably often meant that the disk buffer you wrote to swap is still in RAM when you read it back shortly afterward as part of setting up the new process's memory). So to copy the exec() and exece() arguments, V7 allocates a disk buffer in swap space, copies from user space to the disk buffer until it fills up, flushes and releases the disk buffer, gets a new disk buffer for the new block of swap, and does it all over again. (As an extra complication, V7 didn't have page based swapping, it only swapped whole programs. So during an execve(), V7 allocated swap space for a NCARGS sized 'program' and then used as much of it as necessary, one disk buffer at a time. If V7 couldn't allocate the necessary swap space during exece(), it paniced.) PS: If you look at the V6 code for exec() carefully, you'll see one spot where it does 'suword(ap=+2, c);', which looks odd and wrong. That's because in V6 C, '=+' was how you wrote in-place arithmetic, instead of '+=' in V7 and the C we know today. These are my WanderingThoughts (About the blog) This is part of CSpace, and is written by ChrisSiebenmann. Mastodon: @cks Twitter @thatcks Categories: links, linux, programming, python, snark, solaris, spam, sysadmin, tech, unix, web Also: (Sub)topics Source: https://www.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/sys1.c utcc.utoronto.ca · tuhs.org
When I wrote up how V7 gave us environment variables, I mentioned that up to V6, exec(2)
had a limit of 510 bytes of command line arguments (including argv[0],
the nominal name of your program). You can see the check in the V6
kernel exec() code in sys/ken/sys1.c
(where it returns E2BIG in this case). You might wonder where this
limit comes from and why.

When you exec() something, you discard your current process's memory
and address space to create create a new one for the new program. Your
current (user) memory includes the argv you're passing to exec(), so
the kernel has to copy it from your user space into the kernel and
then back, temporarily holding it in some sort of kernel memory. In a
modern kernel you might dynamically allocate this kernel memory in
exec() through the kernel equivalent of malloc(), but the Research
Unix kernels were simple and didn't have that sort of thing. Instead,
through Research Unix V6, they got their temporary scratch space for
exec() by allocating a disk buffer, reusing a facility the kernel
already needed. These disk buffers were 512 bytes long, which is more
or less where the 510 byte limit on argument size comes from.

(I don't know why it's 510 bytes instead of 512; I've been unable to
follow the code closely enough to see if it slips in a use of the last
two bytes of the buffer for something else.)

You might innocently think that using a disk buffer just pushes the
problem of dynamic allocation of (kernel) memory back one layer, to
the disk buffer system. However, early Research Unix kernels are more
brute force than that. The V6 kernel has a fixed (and limited) chunk
of memory reserved for disk buffers, the buffers array in
sys/dmr/bio.c,
with its size set by NBUF in sys/param.h. The
default NBUF isn't very large, but early Research Unix ran on small
systems and had low limits in general (the same param.h sets a limit
of 50 processes for the entire system).

This straightforward approach to exec() and disk buffers goes back
to at least Research Unix V4 (I haven't looked earlier than that). In
V7, the kernel implementation is rather more complex
because it needs to handle environment variables too, but it still sort of uses the disk
buffer trick. In order to get the extra space without using much extra
RAM, V7 uses swap space, writing to it and reading back from it
through V7's general disk buffer system (which probably often meant
that the disk buffer you wrote to swap is still in RAM when you read
it back shortly afterward as part of setting up the new process's
memory). So to copy the exec() and exece() arguments, V7 allocates
a disk buffer in swap space, copies from user space to the disk buffer
until it fills up, flushes and releases the disk buffer, gets a new
disk buffer for the new block of swap, and does it all over again.

(As an extra complication, V7 didn't have page based swapping, it only
swapped whole programs. So during an execve(), V7 allocated swap
space for a NCARGS
sized 'program' and then used as much of it as necessary, one disk
buffer at a time. If V7 couldn't allocate the necessary swap space
during exece(), it paniced.)

PS: If you look at the V6 code for exec() carefully, you'll see one
spot where it does 'suword(ap=+2, c);', which looks odd and wrong.
That's because in V6 C, '=+' was how you wrote in-place arithmetic, instead of '+=' in V7
and the C we know today.

These are my WanderingThoughts
(About the blog)

This is part of CSpace, and is written by ChrisSiebenmann.
Mastodon: @cks
Twitter @thatcks

Categories: links, linux, programming, python, snark, solaris, spam, sysadmin, tech, unix, web
Also: (Sub)topics

Comments

Log in Log in to comment.

No comments yet.