A little question about function address with no-pie on RISCV
ywgrit
wangxin03@loongson.cn
Thu Nov 28 06:38:30 GMT 2024
在 2024/11/28 上午11:13, Xi Ruoyao 写道:
> On Thu, 2024-11-28 at 10:18 +0800, ywgrit wrote:
>> Thank you very much.
>>
>> 在 2024/11/27 下午7:21, Xi Ruoyao 写道:
>>> Fangrui has left Google.
>>>
>>> On Wed, 2024-11-27 at 18:00 +0800, ywgrit wrote:
>>>> 1) The program tested on both riscv and x86_64.
>>>>
>>>> // foo.c
>>>>
>>>> void foo() {}
>>>>
>>>>
>>>> // main.c
>>>>
>>>> #include <stdio.h>
>>>> void foo();
>>>>
>>>> int main() {
>>>> foo();
>>>> printf("%p\n", foo);
>>>> return 0;
>>>> }
>>>>
>>>> 2) The commands executed.
>>>>
>>>> gcc -fpic -shared -g -o libfoo.so foo.c
>>>>
>>>> gcc -c main.c -o main.o
>>>>
>>>> gcc main.o -o main -lfoo -L. -no-pie
>>>>
>>>> 3) The result.
>>>>
>>>> // riscv
>>>>
>>>> ./main
>>>>
>>>> 0x120000530
>>>>
>>>> // x86_64
>>>>
>>>> ./main
>>>>
>>>> 0x7ffff3ea05a0
>>>>
>>>> 4) The question
>>>>
>>>> With no-pie, riscv print the address of foo@plt which is in main, x86_64
>>>> print the address of foo which is in libfoo.so.
>>> On x86_64 if you use
>>>
>>> gcc main.o -o main -lfoo -L. -no-pie -fno-pie
>>>
>>> you get the address of the PLT entry too. -fno-pie controls GCC code
>>> generation, while -no-pie is directly passed to the linker and mostly
>>> ignored by GCC.
>>>
>>> Traditionally, in a PDE the relocation for addressing *external* symbols
>>> was resolved at link time. For example, when t.so provides data symbol
>>> dat and function symbol func, and pde.c is:
>>>
>>> int printf(const char *, ...);
>>> extern int dat;
>>> extern int func();
>>>
>>> int main() {
>>> printf("%p\n", &dat);
>>> printf("%p\n", &func);
>>> }
>>>
>>> On x86_64 "gcc pde.c -fno-pie -no-pie t.so -O2" gives;
>>>
>>> 0000000000401060 <main>:
>>> 401060: 48 83 ec 08 sub $0x8,%rsp
>>> 401064: be 20 40 40 00 mov $0x404020,%esi
>>> 401069: bf 04 20 40 00 mov $0x402004,%edi
>>> 40106e: 31 c0 xor %eax,%eax
>>> 401070: e8 bb ff ff ff call 401030 <printf@plt>
>>> 401075: be 40 10 40 00 mov $0x401040,%esi
>>> 40107a: bf 04 20 40 00 mov $0x402004,%edi
>>> 40107f: 31 c0 xor %eax,%eax
>>> 401081: e8 aa ff ff ff call 401030 <printf@plt>
>>> 401086: 31 c0 xor %eax,%eax
>>> 401088: 48 83 c4 08 add $0x8,%rsp
>>> 40108c: c3 ret
>>> 40108d: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
>>> 401094: 00 00 00
>>> 401097: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
>>> 40109e: 00 00
>>>
>>> So the address of dat is "fixed" at 0x404020, the address of func is
>>> "fixed" at 0x401040, etc. But dat and func are actually in t.so which
>>> is loaded elsewhere, so for the data symbol dat, there's a "copy
>>> relocation" in the PDE:
>>>
>>> 000000404020 000400000005 R_X86_64_COPY 0000000000404020 dat + 0
>>>
>>> which instructs ld.so to copy dat to 0x404020 before running the code in
>>> the PDE.
>>>
>>> For the function symbol func, the solution should be obvious: the
>>> "fixed" address 0x401040 is just the address of the PLT entry for func.
>> Is it reasonable to access the address of a function and get the address
>> of the plt of the function?
> As long as the result is same when the function pointer is same when you
> take it from the shared library and the PDE, it's fine. C only requires
> a consistent result of function pointer [in]equality test (== and !=),
> but the result from casting the pointer to an integer (like what printf
> does for %p) or other comparison (<. <=, >, >=) are all implementation-
> defined.
If the correctness requirement means that the function pointers obtained
from the shared library and pde should be same, then
x86_64, riscv64, and loongarch64 all satisfy this requirement and the
function pointers obtained all point to plt entry.
>
> But using PLT address as function pointer value is not fine with
> protected visibility functions, on x86_64 the following case is
> rejected:
>
> $ cat t.c
> extern void *f(void) __attribute__((visibility("protected")));
> void *f(void) { return &f; }
> $ cat main.c
> #include <assert.h>
> extern void *f(void);
> int main() { assert(f() == &f); }
> $ cc t.c -shared -fpic -o t.so
> $ cc main.c t.so -fno-pie -no-pie
> /usr/bin/ld: /tmp/ccridms9.o: non-canonical reference to canonical protected function `f' in t.so
> /usr/bin/ld: failed to set dynamic section sizes: bad value
> collect2: error: ld returned 1 exit status
>
> It's one of the reasons to stop using the address of the PLT as the
> function address. I.e. even on "old" architectures we have stopped to
> do that with PIE, and on LoongArch we shouldn't do it at all.
>
>>> This scheme had seemed useful even for PIE so it had been used for PIE
>>> for a while. But it has some drawbacks, read
>>> https://maskray.me/blog/2021-01-09-copy-relocations-canonical-plt-entries-and-protected
>>> for details. So nowadays (with a recent toolchain) this scheme is no
>>> longer used for PIE by default.
>>>
>>> When LoongArch supported was added, due to all the drawbacks
>>> "R_LARCH_COPY" was NACK'ed very early, thus on LoongArch this scheme
>>> isn't used at all, even for PDE.
>> When compiling ghc e70d41406b5d5638b42c4d8222cd03e76bbfeb86 on
>> LoongArch, because of the -no-pie. The generated ghc runs with an error
>> because when ghc accessing the address of the function, it gets the
>> address of the plt.
>>
>> Therefore it is necessary to link ghc with lld.
> Then there is a bug. I can reproduce something similar with
>
> $ cc t.c -shared -fpic -o t.so
> $ cc main.c t.so -fno-pie -no-pie
> $ LD_LIBRARY_PATH=. ./a.out
> warning: direct reference to protected function `f' in `./t.so' may break pointer equality
> a.out: main.c:5: main: Assertion `f() == &f' failed.
> Aborted
>
> So for a PDE the address of PLT is filled into GOT, breaking equality
> test... not good. To me we should change Glibc ld.so to just fill the
> address of the real function into the GOT instead like how it handles
> PIE (and remove the warning), as on LoongArch we've never had the
> "historical reasons" to justify handle PDE differently from PIE.
>
In this scenario where ghc fails to run on LoongArch64, both ghc(pde)
and the shared library fetch the address of function
'stg_upd_frame_info' and get the address of
stg_upd_frame_info@plt(passed equality teststated above?), but ghc
wanted to get the address of stg_upd_frame_info, hence the program error.
Based on the behavior of riscv64 for example above, I'm guessing that
compile and run ghc on riscv64 (if using llvm as the ghc backend) would
be wrong just like LoongArch64.
I read the code of gnu-ld and dynamic linker, I found that because the
st_value of the stg_upd_frame_info symbol in pde is the address of the
plt entry, dynamic linker will find
stg_upd_frame_info@plt from pde and return it back, whereas if st_value
== 0, dynamic linker won't find stg_upd_frame_info@plt from pde, and
will return the address of stg_upd_frame_info
which is found in shared library. The relevant code for this part is in
the dynamic linker's check_match function.
The example in my original email was a simplified version of this
problem with ghc. And in this example, riscv64 behaved just like
LoongArch64. So if there is a bug on LoongArch, then
riscv probably has a bug too?
Also, if I add -fno-pie to ghc's compilation options, won't ghc run
error on x86_64? I'm going to try that.
More information about the Binutils
mailing list