[PATCH] ld: Disable separate code by default for Linux/x86
hev
r@hev.cc
Mon Mar 16 17:03:23 GMT 2026
On Tue, Mar 17, 2026 at 12:35 AM H.J. Lu <hjl.tools@gmail.com> wrote:
>
> On Mon, Mar 16, 2026 at 9:21 AM hev <r@hev.cc> wrote:
> >
> > On Mon, Mar 16, 2026 at 11:58 PM H.J. Lu <hjl.tools@gmail.com> wrote:
> > >
> > > On Mon, Mar 16, 2026 at 7:20 AM WANG Rui <r@hev.cc> wrote:
> > > >
> > > > Years ago, commit f6aec96dce1d ("ld: Add --enable-separate-code") introduced
> > > > -z separate-code, mainly to avoid mixing code and data in the same segment
> > > > for better cache performance, and enabled it by default for Linux/x86.
> > >
> > > -z separate-code reduces cache pollution and improves security by avoiding
> > > putting read-only data in the same page with code.
> >
> > I see. IIUC, the security aspect you mentioned mainly concerns
> > avoiding an executable ELF header. In that case we probably need to
> > make a trade-off here.
> >
> > > > Today, Linux already has working support for mapping executable code with THP,
> > > > which can noticeably reduce iTLB pressure and bring measurable performance
> > > > benefits.
> > > >
> > > > In practice, I observed that on AArch64 and LoongArch64 distros, ELF executables
> > > > typically place the code PT_LOAD segment first, so both the virtual address and
> > > > file offset are 0. That turns out to be an important prerequisite for THP-aware
> > >
> > > Should ld.so map read-only data segment plus code segment into the same huge
> > > page? This should have the similar effect as -z noseparate-code.
> >
> > A huge page has a single set of permissions, so it doesn't seem
> > possible for part of the page to be read-only and non-executable while
> > another part is executable.
>
> With -z separate-code, I got
>
> Program Headers:
> Type Offset VirtAddr PhysAddr
> FileSiz MemSiz Flg Align
> PHDR 0x000040 0x0000000000000040 0x0000000000000040
> 0x0002d8 0x0002d8 R 0x8
> INTERP 0x001000 0x0000000000001000 0x0000000000001000
> 0x00001c 0x00001c R 0x1
> [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
> LOAD 0x000000 0x0000000000000000 0x0000000000000000
> 0x0004a1 0x0004a1 R E 0x1000
> LOAD 0x001000 0x0000000000001000 0x0000000000001000
> 0x0003f0 0x0003f0 R 0x1000
> LOAD 0x001d98 0x0000000000002d98 0x0000000000002d98
> 0x000274 0x000278 RW 0x1000
Huh? Why is the layout you get different from mine? With -z
separate-code, the read-only segment should normally come first, so
that the ELF header and program headers fall into that segment and
remain read-only and non-executable for security reasons.
With -z separate-code, I got
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000310 0x0000000000000310 R 0x8
INTERP 0x0000000000000374 0x0000000000000374 0x0000000000000374
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000005c8 0x00000000000005c8 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000139 0x0000000000000139 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000000c8 0x00000000000000c8 R 0x1000
LOAD 0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
0x0000000000000200 0x0000000000000208 RW 0x1000
With -z noseparate-code, I got
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x0000000000000304 0x0000000000000304 0x0000000000000304
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000768 0x0000000000000768 R E 0x1000
LOAD 0x0000000000000e10 0x0000000000001e10 0x0000000000001e10
0x0000000000000200 0x0000000000000208 RW 0x1000
>
> With -z noseparate-code, I got
>
> Program Headers:
> Type Offset VirtAddr PhysAddr
> FileSiz MemSiz Flg Align
> PHDR 0x000040 0x0000000000000040 0x0000000000000040
> 0x0002a0 0x0002a0 R 0x8
> INTERP 0x000304 0x0000000000000304 0x0000000000000304
> 0x00001c 0x00001c R 0x1
> [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
> LOAD 0x000000 0x0000000000000000 0x0000000000000000
> 0x000858 0x000858 R E 0x1000
> LOAD 0x000d98 0x0000000000001d98 0x0000000000001d98
> 0x000274 0x000278 RW 0x1000
>
> Please remind me of the benefits of -z noseparate-code. If ld.so maps
>
> LOAD 0x000000 0x0000000000000000 0x0000000000000000
> 0x0004a1 0x0004a1 R E 0x1000
> LOAD 0x001000 0x0000000000001000 0x0000000000001000
> 0x0003f0 0x0003f0 R 0x1000
>
> in the same huge page with RW permission, will we get the same benefits?
One benefit of -z noseparate-code is that the executable PT_LOAD ends
up at virtual address 0 with file offset 0. For huge page mappings,
both the virtual address and the file offset need to be aligned to the
huge page size, e.g.
((p_vaddr | p_offset) & (HUGE_PAGE_SIZE - 1)) == 0
Are you suggesting that, with the -z separate-code layout, ld.so would
ignore the non-executable flag of the first PT_LOAD and merge it with
the second PT_LOAD, mapping them together as read-only executable?
That might help with existing binaries, but wouldn't it blur the
permission boundaries between segments?
Thanks,
Rui
More information about the Binutils
mailing list