Repository navigation
[GNU ObjC] Clang 14 regression: class methods always return nil object with ARC and optimization #56952
Description
Activity
Additionally I’m seeing some very strange behavior calling class methods in GNUstep Base with Clang 14 and 15rc1 using ARC and -O1 or -O2, which might be related to the above:
@try { id processInfoClass = [NSProcessInfo class]; NSLog(@"processInfoClass: %@ (%p)", processInfoClass, processInfoClass); id processInfo = [NSProcessInfo processInfo]; NSLog(@"processInfo: %@ (%p)", processInfo, processInfo); id arguments = [processInfo arguments]; NSLog(@"arguments: %@", arguments); } @catch (id exc) { NSLog(@"Exception: %@", exc); }
This will result in the following exception when calling
[NSProcessInfo processInfo], which seems to indicate that theprocessInfomethod is called onNSObjectand notNSProcessInfo:<NSException: 0x8d20a068> NAME:NSInvalidArgumentException REASON:NSObject(class) does not recognize processInfo INFO:(null)
I can work around that by replacing
[NSProcessInfo processInfo]with[processInfoClass processInfo], but then I’m getting the following exception when calling[processInfo arguments]in the next line, where theargumentsmethod seems to be called on theNSProcessInfoclass instead of an object of that class:<NSException: 0xebb88048> NAME:NSInvalidArgumentException REASON:NSProcessInfo(class) does not recognize arguments INFO:(null)
In summary it seems like there’s some mixup with what kind of class objects the compiler returns, but it depends on the exact code, e.g. when calling just one of these methods above individually it works fine.
- addedclang:codegenIR generation bugs: mangling, exceptions, etc.IR generation bugs: mangling, exceptions, etc.and removed
on Aug 19, 2022 @llvm/issue-subscribers-clang-codegen
Is this Windows-only? Can you run clang with
-mllvm -print-after-alland see where the message send is either being elided or where the receiver becomes 0?My guess is that LLVM is seeing that the class global variable is initialized to a null pointer and somehow missing the fact that the function that sets it is called from the runtime.
Looks like this is a Windows-only issue, at least we were unable to reproduce it on Debian 11 aarch64 using Clang 15.0.0-rc2.
This is the LLVM IR dump of a further reduced test case printed below, though unfortunately I’m not fully sure how to read it:
llvm-output.txt#import "objc/runtime.h" __attribute__((objc_root_class)) @interface TestObj { id isa; } + (Class)class; + (id)testObj; @end @implementation TestObj + (Class)class { return self; } + (id)testObj { id testObj = @"Test"; return testObj; } @end int main() { TestObj *testObj = [TestObj testObj]; return testObj ? 0 : 1; }
- added a commit that references this issue
on Sep 21, 2022 One quick find in the assembly of the
libobjc2test above: There are return values that clang-14 copies from%raxto%rdiregister while clang-13 used to copy to%rcx-- so it becomes first parameter for the subsequent call.Here is an annotated assembly diff that omits some non-printable characters from the
objc_selectors:-ClassMethods_arc_clang13_release.m.obj: file format coff-x86-64 +ClassMethods_arc_clang14_release.m.obj: file format coff-x86-64 @@ -92,7 +92,7 @@ 7: 48 8b 0d 00 00 00 00 movq (%rip), %rcx # $_OBJC_REF_CLASS_TestObj e: 48 8d 15 00 00 00 00 leaq (%rip), %rdx # .objc_selector_class___ (1) 15: e8 00 00 00 00 callq 0x1a <main+0x1a> # objc_msgSend - 1a: 48 89 c1 movq %rax, %rcx + 1a: 48 89 c7 movq %rax, %rdi 1d: e8 00 00 00 00 callq 0x22 <main+0x22> # objc_retainAutoreleasedReturnValue 22: 48 89 c6 movq %rax, %rsi 25: 48 89 c1 movq %rax, %rcx @@ -104,7 +104,7 @@ 40: 48 8b 0d 00 00 00 00 movq (%rip), %rcx # $_OBJC_REF_CLASS_TestObj 47: 48 8d 15 00 00 00 00 leaq (%rip), %rdx # .objc_selector_testObj___ (2) 4e: e8 00 00 00 00 callq 0x53 <main+0x53> # objc_msgSend - 53: 48 89 c1 movq %rax, %rcx + 53: 48 89 c7 movq %rax, %rdi 56: e8 00 00 00 00 callq 0x5b <main+0x5b> # objc_retainAutoreleasedReturnValue 5b: 48 89 c7 movq %rax, %rdi 5e: 48 8d 0d 00 00 00 00 leaq (%rip), %rcx
The difference is in two places between the calls to
objc_msgSendandobjc_retainAutoreleasedReturnValue:-
After
objc_selector_class___it looks like it "silently works" becauseobjc_msgSendstored its return value0x00007ff6979b58e0in%rcxas well (or just doesn't clear the register before returning). Thenobjc_retainAutoreleasedReturnValuereturns it again and we correctly print the line: testClass: TestObj (00007FF6979B58E0) -
After
objc_selector_testObj___theobjc_msgSendcall behaves differently and%rcxis null. Thenobjc_retainAutoreleasedReturnValuereturns null as well and we incorrectly print the line: testObj: 0x0000000000000000
-
I used the above assembly difference for bisectioning the clang history and arrived at this commit: d61eb6c
On the current release/15.x version, I tested a change that lets Windows targets opt-out from using
clang.arc.attachedcalloperand bundles and it does successfully prevent this issue: weliveindetail@d2c3ec2My guess is that this can only be a temporary fix as it likely has an impact on performance of compiled output. A proper solution would teach the x86_64 backend for Windows how to handle the operand bundle correctly.
This usage of RDI seems like the bug:
https://github.com/llvm/llvm-project/blob/main/llvm/lib/Target/X86/X86ExpandPseudo.cpp#L229That was added in c2d44bd by @fhahn . The first parameter register on Windows is RCX, not RDI.
As discussed in https://reviews.llvm.org/D134441, if the runtime on windows excepts a different marker instruction, we should update
x86ExpandPseudo.cpp. If no marker is needed on Windows, we shouldn't generatedclang.arc.attachedcallin WindowsThanks for your feedback everyone!
This usage of RDI seems like the bug: https://github.com/llvm/llvm-project/blob/main/llvm/lib/Target/X86/X86ExpandPseudo.cpp#L229
That was added in c2d44bd by @fhahn . The first parameter register on Windows is RCX, not RDI.
Yes, I tested this and it gives the correct output as well!
As discussed in https://reviews.llvm.org/D134441, if the runtime on windows excepts a different marker instruction, we should update
x86ExpandPseudo.cpp.On Windows it just looks like a regular call following the x64 calling convention and I didn't see anything in the implementation that inspects instruction opcodes on the caller side (so far).
@davidchisnall Is GNUstep checking for optimized callers like the
callerAcceptsOptimizedReturn()in Apple's ObjC runtime here? https://opensource.apple.com/source/objc4/objc4-818.2/runtime/objc-object.h.auto.htmlIf no marker is needed on Windows, we shouldn't generate
clang.arc.attachedcallin WindowsBasically, this is the attempt from my first review D134441. The bundle only makes sure that the instruction sequence ends up unmodified in the binary right?
Could it have any other/non-obvious side-effects? There are several places that check for the presence of the bundle. Most of them appear to make sure the instruction sequence stays intact, but is it the case for the following two as well?
https://github.com/llvm/llvm-project/blob/release/15.x/llvm/lib/Transforms/Scalar/SCCP.cpp#L133-L139
https://github.com/llvm/llvm-project/blob/release/15.x/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp#L246-L249Also, it looks like the inliner can insert these bundles as well -- would I need to suppress this?
https://github.com/llvm/llvm-project/blob/release/15.x/llvm/lib/Transforms/Utils/InlineFunction.cpp#L1753-L1762I guess without a marker, GNUstep wouldn't have a chance to implement ObjC ARC autorelease optimization and always miss autorelease elision like this: #31273
14 remaining items
- added a commit that references this issue
on Jan 30, 2024 - added a commit that references this issue
on May 20, 2024 - added a commit that references this issue
on Aug 8, 2024
Using using Automatic Reference Counting and
-O1or better optimization using Clang 14.0.5 or 15.0 rc1 causes class methods returning an ObjC object type to always return a null pointer.This works correctly with Clang 13, or when not using ARC, or when using
-O0optimization. Methods returning C primitive types are not affected.The following code reproduces the issue:
Build command:
Output: