Dr. Brian Robert Callahan
academic, developer, with an eye towards a brighter techno-social life
This one was a little bit of a head scratcher for me, so it is worth sharing.
In D you have a choice between three different compilers:
The frontend and Phobos, the D standard library, are the same across all three compilers. This is good in the sense that, in theory, if your code compiles on one compiler, you know it will compile on the others. The difference between them is in their backends. DMD can only generate code for i386 and amd64 machines, though Walter did just purchase a Mac mini to complete his new aarch64 code generator. GDC and LDC can generate code for any of the targets supported by GCC and LLVM, respectively. But in practice you are limited to architectures supported by Phobos. I have not found an architecture I want to use that Phobos does not support. Off the top of my head I can think of: i386, amd64, aarch64, armv7, riscv(64), powerpc(64(le)), and sparc64. I suppose that leaves out the mips(64)(el), m68k, and 6502 people. But it is still most architectures for the vast majority of situations.
I noticed that FreeBSD has an LDC package but not a GDC or DMD package. I imagine most people are probably using LDC for their D work, and there is an official DMD package maintained by upstream if you want to have DMD on your FreeBSD machine. This makes GDC the odd one out on FreeBSD and I thought perhaps I could fix that.
GDC is written in D, so you need a D compiler to build GDC. More specifically, you need GDC to build GDC due to GDC usng GNU-style flags, whereas DMD and LDC use Digital Mars-style flags. I suppose you could write a shell script or something to smooth over the differences in flags.
From GCC 9 to GCC 11, GDC was written in C++, as it is the policy of GCC for new languages to initially be written in a language that GCC can already compile. I think that is a good policy. The only GCC frontends written in their own language are C++, Ada, and D anyway; the rest are written in C or C++ (or some combination).
So we can download GCC 11.5.0, the last version of GCC with the C++-written GDC, build that, and then use it to bootstrap a newer GDC. It's an extra step but not just a little bit of time; to save time I built it with --disable-bootstrap
to avoid building a stage2 and stage3 compiler. I'm not keeping GCC 11.5.0 so I don't care if it can rebuild itself. All I care about is that it can build stage1 of the latest version of GDC.
That went off mostly without a hitch; I had to teach DRuntime about FreeBSD 14, but that is an obvious one-line addition. Now I could build GDC 15.2.0.
Or so I thought...
For the curious: in DRuntime, they implemented a check for FreeBSD 14 and newer.
FreeBSD 14 changed the signature of their qsort_r(3)
to match POSIX. They added some compatibility so that if you have old code that uses the old function signature, it will still work (but you should update your code to use the POSIX signature).
GDC is looking to provide that same compatibility. But somehow, something goes wrong in the process. When you are in stage2, you get this error:
/bin/sh ../libtool --tag=D --mode=compile /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/gdc -B/opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/ -B/usr/x86_64-unknown-freebsd14.3/bin/ -B/usr/x86_64-unknown-freebsd14.3/lib/ -isystem /usr/x86_64-unknown-freebsd14.3/include -isystem /usr/x86_64-unknown-freebsd14.3/sys-include -fno-checking -prefer-pic -fversion=Shared -Wall -frelease -ffunction-sections -fdata-sections -O2 -g -fpreview=dip1000 -fpreview=fieldwise -fpreview=dtorfields -nostdinc -I /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/../gcc-15.2.0/libphobos/libdruntime -I . -c -o core/internal/qsort.lo /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/../gcc-15.2.0/libphobos/libdruntime/core/internal/qsort.d libtool: compile: /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/gdc -B/opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/ -B/usr/x86_64-unknown-freebsd14.3/bin/ -B/usr/x86_64-unknown-freebsd14.3/lib/ -isystem /usr/x86_64-unknown-freebsd14.3/include -isystem /usr/x86_64-unknown-freebsd14.3/sys-include -fno-checking -fversion=Shared -Wall -frelease -ffunction-sections -fdata-sections -O2 -g -fpreview=dip1000 -fpreview=fieldwise -fpreview=dtorfields -nostdinc -I /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/../gcc-15.2.0/libphobos/libdruntime -I . -c /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/../gcc-15.2.0/libphobos/libdruntime/core/internal/qsort.d -fPIC -fversion=Shared -o core/internal/.libs/qsort.o /tmp//ccrOAvMh.s: Assembler messages: /tmp//ccrOAvMh.s:41: Error: junk `@FBSD_1.0@PLT' after expression gmake[5]: *** [Makefile:2603: core/internal/qsort.lo] Error 1
Perhaps the mangling is subtly incorrect? To check, I reinvoked that first line adding the -S
flag to generate assembly. When I tried to assemble the result with GNU as, I got the same error message. But when I tried to assemble with Clang, it successfully assembled. That tells me that the mangling is correct; GNU as needs to be taught this construct and what it means.
The problem runs a little deeper. Turns out that GNU as cannot assemble a huge amount of code that is output by FreeBSD's clang. On FreeBSD 14, every piece of code that includes <stdlib.h>
ends up including this line of assembly:
.symver __qsort_r_compat, qsort_r@FBSD_1.0
And GNU as does not understand what that means, so you get an error.
You can try this for yourself: if you create a new C program:
#include <stdlib.h> int main(void){return 0;}
And compile it with cc -S true.c
, you will find that line in the resulting true.s
file. If you try to assemble it with GNU as, you will get this error message:
true.s: Assembler messages: true.s:29: Error: unknown pseudo-op: `.addrsig'
No matter how you slice it, GNU as does not understand the compatibility around qsort_r(3)
on FreeBSD 14 and later.
This does seem a bit of a shame, since we can't compile GDC without making modifications to the GDC source code. It also means that you can't use the -no-integrated-as
flag with clang if you intend to use GNU as for your external assembler. Maybe this doesn't matter so much, since it seems that in reality this problem was never stumbled upon until now, and it only affects a niche implementation of a niche programming language.
I do not believe modifying GDC is a correct answer here; FreeBSD clearly wants the compatibility and DRuntime is right to offer that compatibility.
If clang assembles this code correctly, perhaps we could just use clang as the assembler for GDC? After all, this is exactly what GCC does on macOS: macOS ships a binary named as
but it is really just a symlink to clang.
FreeBSD does not have such a utility, but it is trivial to write one that will do what we want:
#include <sys/wait.h> #include <err.h> #include <stdbool.h> #include <stdlib.h> #include <string.h> #include <unistd.h> extern char **environ; int main(int argc, char *argv[]) { char **av; pid_t pid; int i, status; bool have_input = false, have_output = false; /* * `as' becomes `cc -c -x assembler' * In other words, add 3 to argc. * * If no output file, we need to add another 2 to argc. * `-o' and `a.out' * * If no input file, we need to add another 1 to argc. * `-' * * Plus 1 for the final NULL in all cases. * * Total additional potential args = 7 */ if ((av = reallocarray(NULL, argc + 7, sizeof(char *))) == NULL) err(1, "reallocarray failed"); av[0] = "/usr/bin/cc"; av[1] = "-c"; av[2] = "-x"; av[3] = "assembler"; for (i = 1; i < argc; ++i) { av[3 + i] = argv[i]; if (!strncmp(argv[i], "-o", 2)) have_output = true; if (strncmp(argv[i], "-", 1) != 0) { if (!strcmp(argv[i - 1], "-o")) continue; have_input = true; } } if (!have_output) { av[3 + i] = "-o"; ++i; av[3 + i] = "a.out"; ++i; } if (!have_input) { av[3 + i] = "-"; ++i; } av[3 + i] = NULL; switch ((pid = fork())) { case -1: err(1, "fork failed"); case 0: execve(av[0], av, environ); _exit(127); default: if (waitpid(pid, &status, 0) == -1) err(1, "waitpid"); } i = WIFEXITED(status) ? WEXITSTATUS(status) : 1; free(av); av = NULL; return i; }
You can download this program here.
I installed it as /usr/bin/clang-as
and then started the build of GDC fresh, adding the configure flags --with-as=/usr/bin/clang-as
and --with-gnu-as
. I don't think the --with-gnu-as
flag does anything on FreeBSD but it makes me feel good.
But this did not work either. Once you build the stage1 compiler and start using it, you immediately hit an error:
configure:3576: /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/xgcc -B/opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/ -B/usr/x86_64-unknown-freebsd14.3/bin/ -B/usr/x86_64-unknown-freebsd14.3/lib/ -isystem /usr/x86_64-unknown-freebsd14.3/include -isystem /usr/x86_64-unknown-freebsd14.3/sys-include -fno-checking -o conftest -g -O2 conftest.c >&5 /tmp//ccGF1fUc.s:75:2: error: changed section flags for .eh_frame, expected: 0x2 .section .eh_frame,"aw",@progbits ^ /tmp//ccGF1fUc.s:447:2: error: changed section flags for .debug_str, expected: 0x30 .section .debug_str,"",@progbits ^ /tmp//ccGF1fUc.s:447:2: error: changed section entsize for .debug_str, expected: 1 .section .debug_str,"",@progbits ^ configure:3579: $? = 1 configure:3792: checking for suffix of object files configure:3814: /opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/xgcc -B/opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/./gcc/ -B/usr/x86_64-unknown-freebsd14.3/bin/ -B/usr/x86_64-unknown-freebsd14.3/lib/ -isystem /usr/x86_64-unknown-freebsd14.3/include -isystem /usr/x86_64-unknown-freebsd14.3/sys-include -fno-checking -c -g -O2 conftest.c >&5 /tmp//ccBHaChP.s:43:2: error: changed section flags for .eh_frame, expected: 0x2 .section .eh_frame,"aw",@progbits ^ /tmp//ccBHaChP.s:269:2: error: changed section flags for .debug_str, expected: 0x30 .section .debug_str,"",@progbits ^ /tmp//ccBHaChP.s:269:2: error: changed section entsize for .debug_str, expected: 1 .section .debug_str,"",@progbits ^ configure:3818: $? = 1 configure: failed program was: | /* confdefs.h */ | #define PACKAGE_NAME "GNU C Runtime Library" | #define PACKAGE_TARNAME "libgcc" | #define PACKAGE_VERSION "1.0" | #define PACKAGE_STRING "GNU C Runtime Library 1.0" | #define PACKAGE_BUGREPORT "" | #define PACKAGE_URL "http://www.gnu.org/software/libgcc/" | /* end confdefs.h. */ | | int | main () | { | | ; | return 0; | } configure:3832: error: in `/opt/gcc-bootstrap/gcc-15.2.0/build-gcc-15.2.0/x86_64-unknown-freebsd14.3/libgcc': configure:3835: error: cannot compute suffix of object files: cannot compile See `config.log' for more details
I suppose that means that GCC is outputting incorrect flags for .eh_frame
and .debug_str
. Perhaps FreeBSD needs to teach GCC to output the correct flags for these two things. That might improve GCC on FreeBSD in general, and also permit Clang to be used as the assembler for GCC like on macOS, and it would fix the problem of GDC not building on FreeBSD.
The DMD team only tests commits in their CI system with FreeBSD 13. With no DMD in FreeBSD packages, who knows if DMD is successfully building on FreeBSD 14.
LDC too only tests on FreeBSD 13. However, the LDC in FreeBSD packages is built on FreeBSD 14, so one can surmise that LDC does build on FreeBSD 14.
And presumably no one is testing GDC on FreeBSD at all. The GCC 11 FreeBSD package does ship GDC but it is already expired and probably won't be around much longer. FreeBSD packages of newer versions of GCC don't include GDC, presumably because of the bootstrap issue. GCC 11 does not exhibit this problem because it was released prior to FreeBSD 14, and a such the problem block in DRuntime doesn't exist in this version.
This is why operating system-level package maintainership matters. If there was a FreeBSD port of GDC, this problem would have been caught very quickly. But, of course, that takes time and effort. But if you are waiting to port something to your favorite operating system, don't wait.
I suppose the same is true of maintenance, and I have not done a good job at maintaining my OpenBSD ports. I should fix that, as maintenance is an important aspect of catching these kinds of bugs.
One (or both) of these fixes would work:
qsort_r(3)
compatibility.eh_frame
and .debug_str
so that Clang can be used as the GCC assembler
To be clear, I don't think anyone is "at fault" here. The D people have more pressing issues to deal with than trying every potential combination of every potential platform their compilers run on. And I don't think the FreeBSD people are at fault either; the community packages what it believes will be useful for its users. I am advocating that we work to enlarge that community, and that becoming a package maintainer for your favorite software on your favorite operating system is a good way to help us help us all by ensuring a greater array of testing more code in more situations.
So we didn't get modern GDC up and running on FreeBSD. But that's OK. We figured out what the problem is and brainstormed some solutions. I'm not embedded enough in the FreeBSD community to know which solution they'd prefer, so I will leave deciding to them for now.