Skip to main content

Upgrading a 32-bit Linux system to 64-bit, in-place

For about 5 years now my primary computer has been a Linux system based off of LFS. When I first compiled it, I was hesitant to go 64-bit, because LFS on it's own doesn't provide any directions for making a multilib system, and I knew I would need 32-bit libraries.

So, I made the decision to just go 32-bit. It made sense since I only had 4GB of RAM at the time anyway. I have seen the need for more over the years (running Firefox with multiple pages loaded and running a RAM-heavy game would sometimes bring the computer to a grinding halt), but it was still manageable. But I knew that I would probably eventually upgrade to 64-bit, especially when I could get a multilib setup. I put it off for a while, but two weeks ago I decided to bite the bullet and do it.

The way I figured, it should be relatively simple. If I had a 64-bit kernel with IA32 emulation support, just swapping in the new kernel should let the system boot up as normal, running a 64-bit kernel but with all the old 32-bit binaries and libraries being used. Ideally, it would Just Work™. Naturally, after getting the system up and running, I would want to get some standard 64-bit libraries in place to get multilib applications working as well.

I searched online for any information about how to go about it, but I really couldn't find much. It turns out everyone uses distributions ☺. So I've decided to document my process for that one other person out there who is in the same boat as me. Or just for those who are curious about it...

tl;dr There were some bumps along the way, but upgrading a 32-bit system to 64-bit in-place basically works as one would expect it to.

The basic process is as follows:

  1. Make a 64-bit toolchain for cross-compilation
  2. Cross-compile the kernel (and modules, including graphics drivers!)
  3. Load the system with the new kernel
  4. Build and install a 64-bit compile toolchain and libraries (gotta have that 64-bit C library!)
  5. Build and install 64-bit and 32-bit libraries and applications (I figure it's a good idea to keep the version of your multilib libraries the same...)
  6. Breathe a sigh of relief

Below are some loose instructions on how you might follow the steps I took, and some more details about my experience with this process.

Make a 64-bit toolchain for cross-compilation

This one is pretty standard, and there are lots of examples and tutorials online.

You need to get GNU binutils and compile them so that they will operate on 64-bit binaries. That's usually the painless part. Then you need to get GCC and Glibc, and do a bit of a dance to compile them, since each depends on the other! Glibc needs to be compiled using the compilers that we generate from GCC, and naturally the GCC compilers need a reference C library to use! Also, for Glibc you'll need the header files of whatever kernel you plan to use.

So, you first have to build only the gcc compilers (without libgcc, which is what needs the C library).

$ make all-gcc
$ make install-gcc

And then you can build Glibc headers and some libraries (but not all of them) using the new compilers. After that, you go back and make libgcc and install it, and finally fully make Glibc and install it. Whew. For more details check out this link.

Cross-compile the kernel

To compile Glibc, you should have already had your kernel headers around. Building the kernel is simple. You'll want to run

$ make ARCH=x86_64 menuconfig

which will ensure that the .config file will have the appropriate fields for a 64-bit kernel. Then, set the proper toolchain prefix (based on whatever you did for binutils and gcc/g++) and make sure that the new kernel will be able to support 32-bit binaries with IA32 emulation enabled:

General setup --->
    (your-prefix) Cross-compiler tool prefix

Executable file formats / Emulations --->
    [*] IA32 Emulation
    <*> IA32 a.out support
    [*] x32 ABI for 64-bit mode

The x32 ABI is optional, but it doesn't hurt to have it in case you do decide to compile x32 binaries.

Build the kernel in the 'usual' way, taking care to include ARCH=x86_64 in the make commands. The generated compressed kernel image will actually be at arch/x86/boot/bzImage (or a name based on whatever compression you used for the image), but arch/x86_64/boot/bzImage will be a symlink pointing to that as well.

Now just install the kernel and add an entry in your bootloader to use the new kernel image. You should be able to now boot into a 64-bit system! Albeit, with pieces missing.

Load the system with the kernel

As I said at the end of the previous section, you ought to be able to boot into a 64-bit system now. Some init scripts may not immediately work, and if you had external kernel modules, you'll have to rebuild those since they need to be 64-bit now. But, you should be able to get in, at least to a terminal. The first time I booted, I had forgotten to enable IA32 emulation, so this quickly led to the kernel complaining that it couldn't launch init (a 32-bit binary at the time). But that was simple to fix.

If you're lucky, you will be able to get your X display server running immediately. I was not so lucky. The difficulty arose because I have an Nvidia graphics card, so I had to use an external kernel module. However, the normal Nvidia driver installer assumes that your environment and kernel will be the same architecture as your X server! That is a reasonable assumption to make, but in this case I needed to compile a 64-bit module for the graphics card, but a 32-bit X server module. I solved this by getting the 32-bit Nvidia release, which had a nice 32-bit X server module sitting pre-compiled for me to copy. Another quick solution, just to get a graphical environment (if you want it) is to change the X server driver to a generic one that doesn't need a kernel module, such as VESA.

I've since recompiled my X server to be 64-bit, with 32-bit libraries installed for multilib compatibility.

Build and install a 64-bit compile toolchain and libraries

At this point, once you're in a 64-bit world, you'll want a toolchain which is adjusted for the paths of the new system. I decided to put all 64-bit binaries in /lib64 and /usr/lib64, allowing the 32-bit binaries to remain in /lib and /usr/lib. Then, just build and install GNU binutils, Glibc, and GCC (for starters) with the lib directory set appropriately.

The helpful guidance and tips from William Feely were invaluable in getting all the pieces working, as he's taken the LFS guide and has been making adjustments so that someone can generate a multilib LFS system. His BLFS guide and wiki were helpful as well, although at the time of this writing the BLFS guide was mostly just like the normal BLFS. The wiki was documenting the changes he was going to make to the BLFS guide.

Only a little while into my process of compiling native libraries did I realize that something was definitely wrong with my g++ cross-compiler. I'm not sure what went wrong there, but after doing some careful compilation, I was able to compile my new Glibc, gcc, and g++, and then use the new ones (which were correct as far as make test is concerned) to generate those libraries that were affected. The symptoms were, needless to say, some segfaults and all kinds of weird things, normally involving (from what I could tell) pthreads. I'm sure there were some other issues too. For instance, the defective g++ cross-compiler was unable to generate exceptions! Well, it would generate them, but the c++ runtime would always experience a SIGABRT when encountering them, so that wasn't good at all. I mean, having an entire, IMPORTANT feature of the c++ language not work at all would definitely generate some buggy libraries!

On another note, ALWAYS run make test or make check, where appropriate, for the basic binaries and libraries of the system. It's much better to find out early that your system has a buggy binary or library rather than after it is integrated and used by other parts of the system. This might not be as important for more specific binaries or libraries that you install, but it is essential for the libraries that will be used very often, or have critical functionality.

Build and install 64-bit and 32-bit libraries and applications

Like I said in the summary, it's a good idea to install for both 64-bit and 32-bit, since having your multilib libraries at the same version and API and all that (rather than just keeping whatever version you previously had on the 32-bit system) is a good idea. So just go through the fun process of installing the basic libraries and applications you'll be using. So far, I've installed the X server (and libraries), GTK2, Python2 and Python3, fluxbox, and a handful of other libraries and binaries which I felt should be 64-bit.

I have kept a lot of the 32-bit binaries I had (because the whole point of this way of upgrading was to take advantage of what I already had), but I'll upgrade them to be 64-bit binaries on an as-needed basis. So far though, everything is pretty stable! is your friend

My recommendation for this stage is to set up some useful files. I placed them at /etc/ and /etc/, and set CONFIG_SITE=/etc/ to be exported in my shell startup scripts. Then, compiling the 64-bit version of any autoconf package can be done with:

$ ./configure

and compiling the 32-bit version is as simple as:

$ env CONFIG_SITE='/etc/' ./configure

For reference, my looks something like this:

test "$prefix" = NONE && prefix='/usr'
test "$libdir" = '${exec_prefix}/lib' && libdir='${exec_prefix}/lib64'
test "$libdir" = '${prefix}/lib' && libdir='${prefix}/lib64'

if test "$prefix" = '/usr';
    test "$sysconfdir" = '${prefix}/etc' && sysconfdir='/etc'
    test "$sharedstatedir" = '${prefix}/com' && sharedstatedir='/var'
    test "$localstatedir" = '${prefix}/var' && localstatedir='/var'

if test "$prefix" = '/usr/local';
    test "$sysconfdir" = '${prefix}/etc' && sysconfdir='/etc'
    test "$sharedstatedir" = '${prefix}/com' && sharedstatedir='/var'
    test "$localstatedir" = '${prefix}/var' && localstatedir='/var'

export PKG_CONFIG_LIBDIR="/usr/lib64/pkgconfig"

My is similar, except it doesn't change the libdir at all, and defines

CC="gcc -m32"
CXX="g++ -m32"
export PKG_CONFIG_LIBDIR="/usr/lib/pkgconfig"

at the end instead. It probably should set the autoconf build argument too, but so far I've had success with just setting gcc and g++ to compile for 32-bit for almost every package. I think there were one or two where I had to specify --build=i386-pc-linux-gnu, but I can't remember exactly which ones.

The PKG_CONFIG_LIBDIR definitions are necessary so packages that use pkg-config to see what you have installed (which is MANY of them) don't think you have a 64-bit library installed when you really only have the 32-bit version installed, and visa versa. It also ensures that the package will link against the correct version. It took me a little while (and a few botched builds) to realize this issue.

Python bites

All right, cute titles aside, Python really is annoying for multilib systems, because they hard-code the lib directory! The autoconf libdir variable is used for installation, but no matter what that is, the lib directory Python will look in for modules will always be {prefix}/lib/python{version}/..., and this isn't defined in a single place for you to change so that, for instance, our 64-bit python will look in {prefix}/lib64/.... However, after a bit of reading through the files, I was able to figure out a set of changes which, so far, has worked well for me. Note that this is necessary for both Python2 and Python3, and I didn't make a patch file for the reason that it would (probably) only work for the single version(s) of Python that I installed.

You should make changes to the following files after running configure. Also, the Python configure script doesn't properly propagate the configure variables loaded from, so you should specify the prefix, libdir, sysconfdir, and other flags manually to configure. You should change all of these files so that occurrences of 'lib' are instead 'lib64', when building for 64-bit.

  • Makefile so SCRIPTDIR is correct (you COULD use a shared SCRIPTDIR, but I prefer to keep it separate, since installed modules may have compiled, architecture-specific parts)
  • Modules/getpath.c
  • Modules/Setup (if applicable, for SSL and zlib)
  • Lib/distutils/command/
  • Lib/distutils/
  • Lib/ (optional)
  • Lib/
  • Lib/

After making these changes and installing, you should also link things appropriately to the multiarch wrapper as described on the BLFS wiki for Python2 from William Freely.

Breathe a sigh of relief

You did it! It's a bit of a trek, but I thought it was a good experience, and I learned new details about how gcc sets its default paths, among other things. I had to understand that thoroughly when fixing my broken g++ cross-compiler and creating the new native compilation toolchain. But after it is all said and done, my system is working quite well, and now I can even add more RAM without enabling PAE! So that's always cool.