guile-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

the page size bug


From: Andy Wingo
Subject: the page size bug
Date: Wed, 08 Mar 2017 21:42:06 +0100
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/25.1 (gnu/linux)

Hi,

Guile 2.2 uses ELF to format its object files.  ELF objects are composed
of *sections* and *segments*.  Sections are things like .data, .strtab,
and so on; segments contain sections with similar permissions
(read-only, read-write, etc).  See
https://www.gnu.org/software/guile/docs/master/guile.html/Object-File-Format.html
for more details.

Parts of different sections refer to each other.  For example the
read-only .rtl-text code section refers to mutable values in the .data
section, by relative offset.  It's the linker's job to make all of these
connections, and the loader's job to ensure they are valid when the file
is loaded.

The loader has two paths: an mmap path and a malloc path.  The malloc
path just slurps the file into memory; the relative references in the
file correspond to relative references in memory.  With the malloc path,
everything is mutable from the operating system's point of view.

The problem with the malloc path is that not only is it a bit slower, as
it has to page in data that might not be needed, it doesn't share memory
between processes.  So if the system supports mmap, Guile will use mmap
to load its ELF images (.go files).

The mmap path likewise just mmaps the file into memory.  The operating
system will lazily page in data from disk as needed.  Writable segments
ends up being process-local, but read-only segments can share memory
between different processes.  This lowers Guile's resident memory
footprint, especially when many Guile processes are live.

What ends up happening in the mmap case is that the whole file is mapped
into memory, then the writable segments are made writable via
mprotect().  This only works if the writable segments are page-aligned.

And with that prelude out of the way, here's the bug: Guile currently
assumes that 4096 is a multiple of the page size.  So the mprotect fails
to make the segment writable, and runtime relocations that mutate the
segment cause segmentation faults.

One solution to this issue would be to choose target-specific page
sizes.  This is still a little tricky; on amd64 for example, systems
commonly have 4KB pages, but they are allowed by the ABI to have any
multiple-of-2 page size up to 64 KB.  On Cygwin, pages are 4kB but they
can only be allocated 16 at a time.  MIPS and ARM64 can use 64K pages
too and that's not uncommon.

At the current time, in Guile we would like to reduce the number of
binaries we ship to the existing 32-or-64-bit and big-or-little-endian
variants, if possible.  It would seem that with the
least-common-multiple of 64 KB pages, we can do that.

See https://github.com/golang/go/issues/10180 for a discussion of this
issue in the Go context.

So my proposal is to increase the page size to which we align our
segments to 64KB.  This will increase the size of our .go files, but not
the prebuilt/ part of the tarball as that part of the file will be
zeroes and compress well.  Additionally on a system with 4KB pages, the
extra padding will never be paged in, nor read from disk (though it
causes more seeking etc so on spinning metal it's a bit of a lose).

On many 64-bit platforms, binutils currently defaults to aligning
segments on 2MB boundaries.  It does so by making the file and the
memory images not the same: the pages are all together on disk, but then
when loading, the loader will mmap a region "memsz" large which might be
greater than the file size, then map segments into that region.  I would
like to avoid this complication for now.  We can consider adding it in
the future in a compatible way in 2.2 if it is important.

Thoughts welcome :)  I am going to give this a go now.

Andy



reply via email to

[Prev in Thread] Current Thread [Next in Thread]