官术网_书友最值得收藏!

3.4 PC相對尋址

程序計數器(Program Counter,PC)用來指示下一條指令的地址。為了保證CPU正確地執行程序的指令代碼,CPU必須知道下一條指令的地址,這就是程序計數器的作用,程序計數器通常是一個寄存器。例如,在程序執行之前,把程序的入口地址(即第一條指令的地址)設置到PC寄存器中,CPU從PC寄存器指向的地址取值,然后依次執行。CPU執行完一條指令后會自動修改PC寄存器的內容,使其指向下一條指令的地址。

RISC-V指令集提供了一條PC相對尋址的指令AUIPC,格式如下。

auipc rd, imm

這條指令把imm(立即數)左移12位并帶符號擴展到64位后,得到一個新的立即數。這個新的立即數是一個有符號的立即數,再加上當前PC值,然后存儲到rd寄存器中。由于新的立即數表示的是地址的高20位部分,并且是一個有符號的立即數,因此這條指令的尋址范圍為基于當前的PC偏移量±2 GB,如圖3.6所示。另外,由于這個新的立即數的低12位都是0,因此它只能尋址到與4 KB對齊的地址。涉及4 KB以內的尋址,則需要結合其他指令(如ADDI指令)來完成。

圖3.6 AUIPC指令尋址范圍

另外,還有一條指令(LUI指令)與AUIPC指令類似。不同點在于LUI指令不使用PC相對尋址,它僅僅把立即數左移12位,得到一個新的32立即數,再帶符號擴展到64位,將其存儲到rd寄存器中。AUIPC和LUI指令的編碼如圖3.7所示。

圖3.7 AUIPC和LUI指令的編碼

【例3-5】 假設當前PC值為0x8020 0000,分別執行如下指令,a5和a6寄存器的值分別是多少?

auipc   a5,0x2
lui     a6,0x2

a5寄存器的值為PC + sign_extend(0x2 << 12) = 0x8020 0000 + 0x2000 = 0x8020 2000。

a6寄存器的值為0x2 << 12 = 0x2000。

AUIPC指令通常和ADDI指令聯合使用來實現32位地址空間的PC相對尋址。AUIPC指令可以尋址與被訪問地址按4 KB對齊的地方,即被訪問地址的高20位。ADDI指令可以在[?2048, 2047]范圍內尋址,即被訪問地址的低12位。

如果知道了當前的PC值和目標地址,如何計算AUIPC和ADDI指令的參數呢?在圖3.8中,offset為地址B與當前PC值的偏移量,地址B與4 KB對齊的地方為地址A,地址A與地址B的偏移量為lo12。lo12是有符號數的12位數值,取值范圍為[?2048, 2047]。

圖3.8 使用AUIPC和ADDI指令尋址

根據上述信息,可以得出計算hi20和lo12的公式。

hi20 = (offset >> 12) + offset[11]
lo12 = offset & 0xfff

這里特別需要注意如下幾點。

hi20表示地址的高20位,用于AUIPC指令的imm操作數中。

lo12表示地址的低12位,用于ADDI指令的imm操作數中。

計算hi20時需要加上offset[11],用于抵消低12位有符號數的影響(見例3-6)。

lo12是一個12位有符號數,取值范圍為[?2048, 2047]。

使用AUIPC和ADDI指令對地址B進行尋址。

auipc a0,hi20
addi a1,a0,lo12

【例3-6】 假設PC值為0x8020 0000,地址B為0x8020 1800,地址B正好在4 KB的正中間,地址B與地址A的偏移量為2048字節,與地址C的偏移量為?2048字節,如圖3.9所示。

圖3.9 地址之間的關系

那我們應該使用地址A還是地址C來計算lo12呢?

應該使用地址C來計算lo12。因為lo12是一個12位的有符號數,取值范圍為[?2048, 2047]。若使用地址A來計算,就會超過lo12的取值范圍。

地址B與PC值的偏移量均為0x1800。根據前面列出的計算公式,計算hi20和lo12。

hi20 = (0x1800 >> 12) + offset[11] = 2
lo12 = 0x800

因為lo12為12位的有符號數,所以0x800表示的十進制數為?2048。下面是訪問地址B的匯編指令。

auipc a0, 2
addi a1, a0, -2048

如果把ADDI指令寫成如下形式,匯編器將報錯。(因為匯編器把字符“0x800”當成了64位數值(即2048)解析,它已經超過了ADDI指令中立即數的取值范圍。)

addi a1, a0, 0x800

報錯日志如下。

AS   build_src/boot_s.o
src/boot.S: Assembler messages:
src/boot.S:6: Error: illegal operands 'addi a1,a0,0x800'
make: *** [Makefile:28: build_src/boot_s.o] Error 1

通常很少直接使用AUIPC指令,因為編寫匯編代碼時不知道當前PC值是多少。計算上述hi20和lo12的過程通常由鏈接器在重定位時完成。不過RISC-V定義了幾條常用的偽指令,這些偽指令是基于AUIPC指令的。偽指令是對匯編器發出的命令,它在源程序匯編期間由匯編器處理。偽指令可以完成選擇處理器、定義程序模式、定義數據、分配存儲區、指示程序結束等功能。總之,偽指令可以分解為幾條指令的集合。與PC相關的加載與存儲偽指令如表3.3所示。

表3.3 與PC相關的加載和存儲偽指令

表3.3中的PIC表示生成與位置無關的代碼(Position Independent Code),GOT表示全局偏移量表(Global Offset Table)。GCC有一個“-fpic”編譯選項,它在生成的代碼中使用相對地址,而不是絕對地址。所有對絕對地址的訪問都需要通過GOT實現,這種方式通常運用在共享庫中。無論共享庫被加載器加載到內存的什么位置,代碼都能正確執行,而不需要重定位(relocate)。若沒有使用“-fpic”選項編譯共享庫,則當有多個程序加載此共享庫時,加載器需要為每個程序重定位共享庫,即根據加載到的位置重定位,這中間可能會觸發寫時復制機制。

【例3-7】 觀察LA和LLA指令在PIC與非PIC模式下的區別。下面是main.c文件和asm.S文件。

<main.c>
 
extern void asm_test(void);
 
int main(void)
{
    asm_test();
 
    return 0;
}
 
<asm.S>
.globl my_test_data
my_test_data:
    .dword 0x12345678abcdabcd
 
.global asm_test
asm_test:
    la t0, my_test_data
    lla t1, my_test_data
 
    ret

首先,觀察非PIC模式。在QEMU+RISC-V+Linux平臺[2]上編譯,使用“-fno-pic”關閉PIC。


[2]QEMU+RISC-V+Linux平臺的搭建方法見第2.4節。

# gcc main.c asm.S -fno-pic -O2 -g -o test

使用OBJDUMP命令反匯編。

root:example_pic# objdump -d test
 
00000000000005f4 <my_test_data>:
 5f4:abcd               j  be6 <__FRAME_END__+0x53e>
 5f6:abcd               j  be8 <__FRAME_END__+0x540>
 5f8:5678               lw a4,108(a2)
 5fa:1234               addi   a3,sp,296
 
00000000000005fc <asm_test>:
 5fc:00000297           auipc  t0,0x0
 600:ff828293           addi   t0,t0,-8 # 5f4 <my_test_data>
 604:00000317           auipc  t1,0x0
 608:ff030313           addi   t1,t1,-16 # 5f4 <my_test_data>
 60c:8082               ret

通過反匯編可知,在非PIC模式下,LA和LLA偽指令都是AUIPC與ADDI指令,并且都直接獲取了my_test_data符號的絕對地址。

接下來,使用“-fpic”重新編譯test程序。

# gcc main.c asm.S -fpic -O2 -g -o test

使用OBJDUMP命令反匯編。

root:example_pic# objdump -d test
 
0000000000000634 <my_test_data>:
 634:abcd               j   c26 <__FRAME_END__+0x53e>
 636:abcd               j   c28 <__FRAME_END__+0x540>
 638:5678               lw  a4,108(a2)
 63a:1234               addi    a3,sp,296
 
000000000000063c <asm_test>:
 63c:00002297           auipc  t0,0x2
 640:9f42b283           ld  t0,-1548(t0) # 2030 <_GLOBAL_OFFSET_TABLE_+0x10>
 644:00000317           auipc  t1,0x0
 648:ff030313           addi   t1,t1,-16 # 634 <my_test_data>
 64c:8082               ret

通過反匯編可知,在PIC模式下,LA偽指令是AUIPC和LD指令的集合,它會訪問GOT,然后從GOT中獲取my_test_data符號的地址;而LLA偽指令是AUIPC和ADDI指令的集合,可直接獲取my_test_data符號的絕對地址。

總之,在非PIC模式下,LLA和LA偽指令的行為相同,都是直接獲取符號的絕對地址;而在PIC模式下,LA指令是從GOT中獲取符號的地址,而LLA偽指令則是直接獲取符號的絕對地址。

【例3-8】 在例3-7的基礎上修改asm.S匯編文件,目的是觀察LI偽指令。

<asm.S>
 
.global asm_test
asm_test:
 
    li t0, 0xfffffff080200000
    ret

在QEMU+RISC-V+Linux平臺上編譯。

# gcc main.c asm.S -O2 -g -o test

使用OBJDUMP命令反匯編。

root:example_pic# objdump -d test
 
00000000000005fc <asm_test>:
5fc:72e1                lui  t0,0xffff8
5fe:4012829b            addiw  t0,t0,1025
602:02d6                slli  t0,t0,0x15
604:8082                ret

從上面的反匯編結果可知,上述的LI偽指令由LUI、ADDIW和SLLI這3條指令組成。

主站蜘蛛池模板: 平谷区| 墨玉县| 沁水县| 广平县| 古蔺县| 合作市| 石棉县| 松溪县| 台前县| 新蔡县| 即墨市| 宜阳县| 文成县| 霍林郭勒市| 库伦旗| 高碑店市| 杭州市| 汽车| 元阳县| 高陵县| 鹿邑县| 沁源县| 罗田县| 桐庐县| 紫云| 罗甸县| 苏尼特左旗| 锦屏县| 讷河市| 香格里拉县| 宜春市| 吴堡县| 乌拉特后旗| 东平县| 永宁县| 宽城| 富顺县| 东安县| 榕江县| 防城港市| 汾阳市|