You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让GCC生成PIC代码时无需通过GOT访问函数局部常量?

问题背景

当使用GCC生成位置无关代码(PIC)时,对于代码中的静态常量数组o,编译器会强制通过全局偏移表(GOT)进行访问,但实际上这种访问并非必要。相关C代码、生成的汇编片段如下:

原始C代码片段

extern const long long *tget(void);

static const signed long long o[2] = { -1, 1 };
long long
tmogrify(long long t)
{
        long long l;
        const signed long long *op;
        long long u = t;
        const long long *lp = tget();

 loop:  
        l = *lp;
        op = o;
        if (l < 0) {
                l = -l;
                ++op;
        }
        // 后续逻辑
}

GCC生成的SPARC汇编(相关片段)

.section        "rodata"
        .align 8
        .type   o , @object
        .size   o , 16
o:
        .long   -1
        .long   -1
        .long   0
        .long   1
        .section        "text"
        .align 4
.LGETPC0:
        retl
        add     %o7, %l7, %l7
        .align 4
        .globl  tmogrify
        .type   tmogrify , @function
        .proc   05
tmogrify:
        !#PROLOGUE# 0
        save    %sp, -104, %sp
        sethi   %hi(_GLOBAL_OFFSET_TABLE_-4), %l7
        call    .LGETPC0
         add     %l7, %lo(_GLOBAL_OFFSET_TABLE_+4), %l7
        call    tget, 0
         nop
        mov     %i0, %o2
        mov     %i1, %o3
.L2:
        sethi   %hi(o), %g2
        ldd     [%o0], %o4 
        or      %g2, %lo(o), %g1
        cmp     %o4, 0
        bge     .L3   
         ld     [%l7+%g1], %g2     // 此处通过GOT访问o
        subcc   %g0, %o5, %o5 
        add     %g2, 8, %g2   
        subx    %g0, %o4, %o4 
.L3:

GCC生成的i386汇编(相关片段)

.section        .rodata
        .align 8
        .type   o , @object
        .size   o , 16
o:
        .quad   -1
        .quad   1
        .text
        .globl  tmogrify
        .type   tmogrify , @function
tmogrify:
        // 省略前序指令
        call    .L6
.L6:
        pop     ebx
        add     ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_+(.-.L6)
        mov     esi, DWORD PTR 8[ebp]
        mov     edi, DWORD PTR 12[ebp]
        call    tget@PLT
        mov     DWORD PTR -20[ebp], eax
.L2:
        mov     eax, DWORD PTR -20[ebp]
        mov     ecx, DWORD PTR 4[eax]  
        mov     edx, DWORD PTR [eax]   
        test    ecx, ecx
        lea     eax, o@GOTOFF[ebx]        // 此处通过GOT访问o
        mov     DWORD PTR -16[ebp], eax
        jns     .L3
        neg     edx
        adc     ecx, 0
        add     eax, 8
        neg     ecx   
        mov     DWORD PTR -16[ebp], eax
.L3:

完整测试模块代码:

extern long long tmogrify(long long);
extern const long long *tget(void);

static const signed long long o[2] = { -1, 1 };
long long
tmogrify(long long t)
{
    long long l;
    const signed long long *op;
    long long u = t;
    const long long *lp = tget();

 loop:
    asm volatile("nop");
    l = *lp;
    op = o;
    if (l < 0) {
        l = -l;
        ++op;
    }
    asm volatile("nop");

    if (l && (t >= l)) {
        asm volatile("nop");
        u += *op;
        asm volatile("nop");
        ++lp;
        goto loop;
    }
    return (u);
}

提问

如何在不脱离标准C范畴(无未定义行为/实现定义行为)的前提下,让GCC优化该代码,避免通过GOT访问函数局部常量数组o?


解决方案

方法1:直接使用字面量替代数组访问

既然数组o的元素是固定的-1和1,完全可以去掉数组,直接在代码中根据条件选择对应常量值。这种方式让编译器无需访问内存,自然不会生成GOT相关指令。

修改后的代码:

extern const long long *tget(void);

long long tmogrify(long long t)
{
    long long l;
    long long u = t;
    const long long *lp = tget();

loop:
    l = *lp;
    long long op_val = -1LL; // 对应o[0]
    if (l < 0) {
        l = -l;
        op_val = 1LL; // 对应o[1]
    }

    if (l && (t >= l)) {
        u += op_val;
        ++lp;
        goto loop;
    }
    return u;
}

方法2:用C11 constexpr强化常量属性(适用于C11及以上)

如果需要保留数组形式,可使用constexpr声明数组,明确告诉编译器这是编译期常量,使其直接内联元素值而非通过内存寻址访问。不过这种方法在部分GCC版本中仍可能生成GOT访问,不如方法1彻底。

示例代码:

extern const long long *tget(void);

static constexpr signed long long o[2] = { -1, 1 }; // C11标准
long long tmogrify(long long t)
{
    long long l;
    const signed long long *op;
    long long u = t;
    const long long *lp = tget();

loop:
    l = *lp;
    op = o;
    if (l < 0) {
        l = -l;
        ++op;
    }

    if (l && (t >= l)) {
        u += *op;
        ++lp;
        goto loop;
    }
    return u;
}

方法3:宏定义封装常量(适用于所有C标准)

如果需要在多处复用这些常量,可使用宏封装,效果和直接使用字面量一致:

#define O_NEG (-1LL)
#define O_POS 1LL

extern const long long *tget(void);

long long tmogrify(long long t)
{
    long long l;
    long long u = t;
    const long long *lp = tget();

loop:
    l = *lp;
    long long op_val = O_NEG;
    if (l < 0) {
        l = -l;
        op_val = O_POS;
    }

    if (l && (t >= l)) {
        u += op_val;
        ++lp;
        goto loop;
    }
    return u;
}

原理说明

GCC生成PIC代码时,默认会通过GOT访问全局/静态变量的地址以保证位置无关性。但如果代码中无需获取变量地址,直接使用编译期可确定的常量值,编译器会将常量直接嵌入指令中,完全跳过内存寻址操作,自然也就避免了GOT访问。方法1从根本上移除了数组指针的使用,是最可靠的解决方案。


内容的提问来源于stack exchange,提问作者mirabilos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 16:05:53