[BUG] GCC 汇编标签地址被意外截断

GCC 汇编标签地址被意外截断

我把这个问题发布在了 Stack Overflow
https://stackoverflow.com/questions/79238937/gcc-assembly-label-address-unexpectedly-truncated-without-warning-in-16-bit-code

问题描述

使用 gcc 处理 16 位汇编程序时遇到了一个奇怪的 BUG

特定情况下,写入寄存器的标签地址超过寄存器位宽时,会被默认截断,没有任何警告与报错。

这是问题的最小样本,将位于 0x8000 的 label 加载进 16-bit 寄存器。

.text
.global _start
.code16

_start:
    movw    $label, %ax

.org 0x8000
label:

以下是编译参数,我使用了 -Ttext=0x8000 设置默认的地址偏移。

gcc -Ttext=0x8000 -Werror -m16 -nostdlib -nostartfiles -no-pie -o test.out test.S

此时,$label 应该等于 0x8000 + 0x8000 = 0x10000。这是一个超过 16-bit 范围的值,但 gcc 并没有报错,而是正常编译。

我使用 objdump 查看反汇编结果,发现 0x10000 直接被截断为了 0x0

objdump -S test.out > test.asm 
test.out:     file format elf32-i386

Disassembly of section .text:

00008000 <_start>:
    8000:	b8 00 00 00 00       	mov    $0x0,%eax
	...

稍后我会证明出现这个问题的特定条件范围。

为了说明这不是 gcc 的正常行为,接下来我会给出几个例子。

1. -Ttext >= 0x10000 || .org >= 0x10000

我将 -Ttext 参数设为 0x10000,且不使用 .org

_start:
    movw    $label, %ax
label:

gcc -Ttext=0x10000 -Werror -m16 -nostdlib -nostartfiles -no-pie -o test.out test.S

gcc 输出了错误信息(这是正常情况):

/tmp/ccFfC1x9.o: in function `_start':
(.text+0x1): relocation truncated to fit: R_386_16 against `.text'
collect2: error: ld returned 1 exit status

或者,使用 -Ttext=0x0.org 0x10000

_start:
    movw    $label, %ax
.org 0x10000
label:

gcc -Ttext=0x0 -Werror -m16 -nostdlib -nostartfiles -no-pie -o test.out test.S

gcc 输出了错误信息(这也是正常情况):

test.S: Assembler messages:
test.S:6: Error: value of 00010000 too large for field of 2 bytes at 00000001

**2. .org < 0x8000 && -Ttext + .org >= 0x10000 **

这次,在 .org 小于 0x8000 的情况下,我们让 -Ttext.org 的值相加大于 0x10000

_start:
    movw    $label, %ax
.org 0x1000
label:

gcc -Ttext=0xF000 -Werror -m16 -nostdlib -nostartfiles -no-pie -o test.out test.S

这本质上和第一种情况是相同的,所以 gcc 也正常地报错了:

/tmp/cceqdykr.o: in function `_start':
(.text+0x1): relocation truncated to fit: R_386_16 against `.text'
collect2: error: ld returned 1 exit status

3. .org >= 0x8000 && -Ttext + .org >= 0x10000

上面几个都是正常的情况,但如果我们让 .org 处于 [0x8000, 0x10000) 的范围内,且 -Ttext.org 的值相加大于 0x10000,事情就变得奇怪了起来。

_start:
    movw    $label, %ax
.org 0x8001
label:

gcc -Ttext=0x8000 -Werror -m16 -nostdlib -nostartfiles -no-pie -o test.out test.S

他应该报错的对吧?毕竟此时 $label = 0x8000 + 0x8001 = 0x10001 明显超出了 16-bit 的范围。但它却成功编译了:

test.out:     file format elf32-i386

Disassembly of section .text:

00008000 <_start>:
    8000:	b8 01 00 00 00       	mov    $0x1,%eax
	...

可以看出 0x10001 被截断为了 0x1 ,这真的很奇怪。

对此,我总结了如下规律

// 使用 start 和 offset 代指 -Ttext 和 .org 的值
// -Ttext = $start
// .org $offset
if start + offset > 0xFFFF
    if start > 0xFFFF
        // 报错
    else if offset > 0xFFFF
        // 报错
    else if offset >= 0x8000
        // 默认截断 start + offset 的值(为什么???)
    else
        // 报错
else
    // 正常计算 start + offset

运行环境

gcc

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/14.2.1/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --enable-languages=ada,c,c++,d,fortran,go,lto,m2,objc,obj-c++,rust --enable-bootstrap --prefix=/usr --lir
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 14.2.1 20240910 (GCC) 

ld

GNU ld (GNU Binutils) 2.43.0

系统

OS: Arch Linux on Windows 10 x86_64
Kernel: 5.15.167.4-microsoft-standard-WSL2

本文发布于2024年11月30日

最后修改于2024年11月30日

posted @ 2024-11-30 14:54  千松  阅读(97)  评论(0)    收藏  举报