Address of `extern global` assumed non-zero?

Clang seems to assume that the addresses of extern global variables are non-zero. However, I cannot find this assumption documented in the LLVM language reference nor does llc seem to optimize based on this assumption.

Is this optimization just happening in the clang frontend or is it baked into LLVM? (I am generating LLVM-IR and have a niche use case where I require an extern global null pointer).

To illustrate the above, consider the following code:

#include <stdio.h>

extern int x;
int main() {
    int *p = &x;
    if (p == 0) printf("hello");

Under clang-15 -O3 -emit-llvm -S test.c the printf is eliminated. Not so if we run clang-15 -emit-llvm -S test.c (to get unoptimized IR which includes the printf) and then llc-15 -O3 test.ll to compile the rest of the way to assembly. For the sake of completeness, I’ll include the following, which is the intermediate code on my system seen by llc:

; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@x = external global i32, align 4
@.str = private unnamed_addr constant [6 x i8] c"hello\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca ptr, align 8
  store i32 0, ptr %1, align 4
  store ptr @x, ptr %2, align 8
  %3 = load ptr, ptr %2, align 8
  %4 = icmp eq ptr %3, null
  br i1 %4, label %5, label %7

5:                                                ; preds = %0
  %6 = call i32 (ptr, ...) @printf(ptr noundef @.str)
  br label %7

7:                                                ; preds = %5, %0
  %8 = load i32, ptr %1, align 4
  ret i32 %8

declare i32 @printf(ptr noundef, ...) #1

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }

!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"Ubuntu clang version 15.0.7"}

Extern globals indeed have a non-null address. To allow a null address you need either weak linkage (if the global may not exist) or -fno-delete-null-pointer-checks (if you legitimately have a global at address zero, e.g. in a kernel context).

Your test with llc doesn’t work for two reasons: First, you are generating IR with optnone attributes. To avoid that use -O3 -Xclang -disable-llvm-optzns instead. Second, you are using llc (which is only for codegen) rather than opt, which is for optimization.

1 Like

Thanks pointing out where I went wrong with llc. In my use case a global address legitimately could be assigned zero at link time.

I tried -fno-delete-null-pointer-checks and while that adds null_ptr_is_valid to the attribute lists of main and printf, the code still gets optimized to return 0—no printf in sight.

You’re probably the lucky person to hit this TODO: llvm-project/ConstantFold.cpp at 2f0a1699eab7c00a64312e7f87e0d85a2e9b9e6e · llvm/llvm-project · GitHub

In which case I can only suggest to declare the symbol as weak, even though it isn’t.