日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

c++定义一个动态全局变量_静态链接与动态链接的宏观概述及微观详解

發布時間:2023/12/10 c/c++ 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c++定义一个动态全局变量_静态链接与动态链接的宏观概述及微观详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

靜態鏈接與動態鏈接的宏觀概述及微觀詳解

第一部分 宏觀概述

1. 靜態鏈接

靜態鏈接就是在程序運行前,鏈接器通過對象文件中包含的重定位表,完成所有重定位操作,并最終形成一個在運行時不需要再次進行依賴庫的加載和重定位操作(因為所有的依賴庫在運行前都被鏈接到程序中了)。

2. 動態鏈接

動態鏈接指的是主程序對動態共享庫或對象中符號的引用,是等到程序運行后再加載并進行重定位操作。程序的主體部分也稱為主程序還是靜態鏈接的,這部分鏈接是不會將依賴的動態共享庫或對象鏈接進主程序的。

2.1 裝載時重定位

裝載時重定位:就是程序在運行的過程中,裝載依賴的動態共享庫或對象并在裝載的過程中完成以下兩種操作:

(1)修正主程序中對動態共享庫或對象中定義的符號的引用,因為這時動態共享庫中所有全局符號的虛擬地址都已確定了。

(2)修正動態共享庫或對象中指令和數據對絕對虛擬地址的引用,因為動態共享庫的虛擬基地址一般都是從0x00開始的,所以當動態共享庫或對象被加載到進程的虛擬地址空間的某地址處(假設加載地址為:virt_so_base),那么它的指令或數據中對絕對虛擬地址的引用就都要加上virt_so_base,這樣就完成動態共享庫或對象中指令和數據的重定位了。

經過以上的操作,大家會發現裝載時重定位雖然解決了動態共享庫或對象可以被動態加載到進程的不同地址空間這一困擾靜態共享庫的難題,但是它還是沒有解決“共享”這個核心問題,也就是同一個動態共享庫或對象不能被多個進程共享這一問題。

例如進程A將DSO加載到自己的地址空間0x1000處,那么DSO中需要重定位的指令和數據就要加上0x1000,而進程B要想將DSO加載到自己的地址空間0x4000處時,就不能共享內存中已有的DSO,因為進程A已經將其重定位了,進程B無法共享。

2.2 PIC機制—解決DSO模塊中指令部分無法共享的問題

關于PIC機制的詳解,我的另一篇文章關于Linux KASLR機制的里面有詳細的關于PIC的介紹。

解決思路:DSO模塊中,指令對本模塊內定義的靜態數據和過程的引用,全都編譯成相對尋址,對全局符號(函數和變量)的訪問,不管是內部定義的還是外部模塊定義的,都通過存儲在RW數據段中的GOT表間接訪問。

(1)如何通過GOT表間接訪問

例如DSO1中定義了call func1這條指令,調用的func1過程是定義在DSO2中的,

這時在編譯DSO1的時候會在DSO1的GOT表中分配一個空表項,并將該空表項相對于

Call func1這條指令的offset存儲在DSO1中call指令的operand位置(相對尋址),該

指令最終會被編譯成call *(offset)這種間接尋址方式,在裝載DSO2后,會修正這個GOT

表項,使其指向DSO2模塊中func1被加載到進程地址空間中實際的虛擬地址,實現代

碼的PIC機制,這樣DSO1的指令部分就能被映射到不同的進程的不同地址空間了,因為它是PIC Code。

(2)數據段中存在的絕對地址引用問題

指針變量是數據段中常見的對某變量的絕對虛擬地址的引用,因此其引用的虛擬地址,會隨著數據段被加載到不同進程的不同地址空間而改變,再加上數據段中的數據值是可變的,其本身就不能被多個進程共享,所以通過裝載時重定位的方式可以解決該問題。

例如:static int a = 3; static int a_p = &a; a_p中存儲的絕對虛擬地址是隨著DSO裝載地址的改變而不停變化的,而且這種數據段每個進程都會有自己的副本的,所以可以用裝載時重定位的方法解決。

(3)DSO內部定義的全局變量和全局函數

DSO內部代碼對其內部自定義的全局變量和全局函數的訪問,統一按照extern方式處理,也就是統一通過GOT表間接訪問,即使它們是定義在同一個DSO內部。

3. 動態鏈接與靜態鏈接的本質區別

動態鏈接是在程序運行過程中,可以根據需要(使用了PLT延遲加載技術),由動態LD按需對依賴的外部函數進行綁定的過程。

靜態鏈接是在程序LD成可執行文件的時候就已經將所有需要重定位的地址修正好了,這也意味著所有的依賴庫都要打包成一個可執行文件,這樣就起不到庫文件在不同進程間共享的作用了。

動態鏈接的本質是:通過GOT表將指令中對(內外)全局符號的引用,轉換成對存儲在RW數據區的GOT表項的間接引用,從而實現指令段的PIC,如果要支持延遲加載技術,那么還需要PLT表輔助,動態LD先通過PLT表,更新相應的GOT表項,然后再通過GOT表項間接訪問外部符號。但是這里有個例外,那就是可執行程序也被稱為主程序,對其內部定義的全局符號的訪問是靜態鏈接的(不作為外部符號處理),對DSO中定義的全局函數的訪問都是通過GOT訪問的,而對DSO中定義的全局變量會根據自身是否是PIC code做出不同的處理方式。

3.1 可執行程序不是PIC code

如果訪問了DSO中定義的全局變量,那么會在可執行程序的.bss段中分配一個同名變量,指令對DSO中該變量的訪問將被更改為對.bss段中新分配的變量的訪問;當DSO被加載后,會將其定義的同名變量的初始值復制到主程序的同名變量(.bss段中新分配的),如果DSO中有對該變量訪問的指令的話,還要將其對應的GOT表項重定位指向主程序的同名變量,這樣該變量最終在程序中就只有一個副本可用。后面會詳解這一過程。

3.2 可執行程序是 PIC code

主程序會在GOT表中建立一個訪問該變量的表項,主程序通過GOT表間接訪問DSO中的該變量。

PIC技術是通過相對尋址和相對間接尋址技術,從而實現指令部分的位置無關,對內部靜態數據和過程的訪問完全可以通過offset相對尋址完成,對(內外)全局數據和過程的訪問可以通過相對offset到GOT再間接尋址;因此無論將它加載到任意虛擬地址空間都能正確訪問對應的數據和方法,但是數據里的絕對地址引用是編譯的時候固定的(例如static int a=10;static int * b=&a,這時a存儲的是b的絕對虛擬地址),所以當加載到不同的虛擬地址空間后,一定要重定位的。

總之一旦指令部分PIC后,其操作數地址(offset)是不變的,變的是數據區。

注意:虛擬地址空間肯定是連續的,但是物理空間不一定連續,這點一定要牢記。

第二部分 ELF文件格式

這里將ELF格式拿出來單獨講,是因為下面第三部分要詳細介紹靜態鏈接和動態鏈接詳細過程了,里面有大量各類表的詳細分析,第一部分中的靜態鏈接關系圖和動態鏈接關系圖詳細描述了靜態鏈接和動態鏈接中各種表之間的關聯關系,至于它們之間如何關聯的以及表中各個字段的含義這就要看ELF的具體協議了。

俞甲子的《程序員的自我修養》這本書關于編譯和鏈接講的非常好,對于ELF文件格式講的也很好,本文就不講ELF文件格式這塊了,有興趣的話可以看這本書并參考下面的鏈接看會更好;因為這本書寫的是32位的編譯與鏈接,64位的有些字段含義是有變化的,例如重定位表中的r_info字段包含兩部分信息;32位:低8位表示重定位類型,高24位表示符號在符號表中的索引,而64位:低32位表示重定位類型,高32位表示符號在符號表中的索引。

https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-54839.html#chapter7-2

這個鏈接可以當作工具書,里面對ELF的講解非常詳細,值得收藏。

ELF headr 包含(program header table) 和 (section header table )

Program header table: 包含每個program segment and relative info (execute view).

Section header table: 包含每個section header and relative info (link view) .

第三部分 靜態鏈接微觀詳解

首先明確一點:源碼先要編譯成目標文件.o,然后再鏈接成可執行文件或共享對象。

編譯階段生成目標文件,目標文件由各個段(section:鏈接視圖概念)構成,段內對函數或變量的引用分為如下兩種類型處理:

(1)全局函數或變量(文件內部或外部定義的全局符號)

統一作為外部符號處理,因此在重定位表中都有相應的描述項

將全局變量統一作為外部符號處理好理解,因為LD合并同類項之后全局變量在數據段(segment:執行視圖)內的offset就變了,編譯的時候不能用相對尋址,更別說絕對尋址了,LD時還要再次進行重定位,所以編譯時統一當作外部符號,由LD統一進行重定位。

將全局函數統一視為外部函數這點和動態共享庫處理內部定義的全局變量的思想是一樣,因為編譯的時候是不知道引用的函數是定義在文件內部的還是文件外部的,例如被引用的函數定義在引用函數的后面的話,這種情況是先編譯引用函數的,這時還是不知道被引用函數是否是定義在本文件,所以在編譯的時候將對函數的引用統一看作是對外部函數的引用來處理,由LD統一進行重定位。

(2)靜態函數或變量(文件內部定義的靜態符號)

分為兩類:靜態變量和靜態函數

靜態變量:靜態變量也會被當作外部符號處理,需要重定位,想想看為什么,還是因為文件中定義的靜態變量(包括其他文件)最終會被合并成可執行文件的一個segment,所以相對地址會變化的,因此編譯的時候就不能相對尋址了(因為LD合并同類項的時候會變的),故直接將其放到重定位表中,讓LD最后進行重定位。

靜態函數是編譯的時候就可以相對尋址的,不需要重定位,想想看為什么,因為靜態函數肯定是定義在同文件中的,因此編譯的時候可以確定函數不是外部函數,它們之間的相對地址是固定的,即使后面的LD過程合并同類項也不會有變化,所以在編譯的時候就可以確定下來了。

所以在編譯的時候對于全局函數,全局變量和靜態變量的訪問都是當作對外部符號訪問來處理的,唯一的例外就是對靜態函數的處理,在編譯的時候就完成地址綁定了。

下面通過舉例詳細介紹

如圖1所示,main.c中定義了全局變量和靜態變量,以及通過全局指針變量和靜態指針變量分別引用全局變量和靜態變量。

下面通過圖2重定位表描述編譯階段是如何處理這些變量和引用的。

圖2 .rela.text重定位表存儲的是text段中哪些地方需要被重定位的相關信息,其中類型1~6是對不同類型變量的引用,其重定位的基地址是有所不同,下面詳細闡述。

3.1 圖2中類型1重定位記錄分析

Offset=0x1a:表明被重定位的位置在.text段中offset=0x1a處,如下圖5中的R1位置。Info=0x001200000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x12)表明重定位入口的符號在符號表中的索引是index=18。

下圖3符號表最左邊一列就是符號在符號表中的索引:

如上圖3所示:num=18就是符號在符號表中的索引,value=0說明該符號在ndx=5(data.rel.local段)中的offset=0,size=8表明該符號占用的空間為8bytes,type=OBJECT表明該符號是一個對象變量,bind=GLOBAL表明該符號綁定的對象是全局的,ndx=5表明該符號所在的段在段表中的索引等于5,參考下圖4的段表可知, ndx=5就是.data.rel.local段。

由上圖2的sym.name+addend=stat_var_gp-4可得addend=-4,根據ELF linkage協議中定義的relocation_type可知,R_X86_64_PC32類型(相對尋址)的重定位公式是:S+A-P. 具體參見下圖6.

S:符號鏈接后最終的虛擬地址

A: 加數(addend,也稱修正值)

P: 需要被重定位處在鏈接后最終的虛擬地址

S+A-P=S-(P-A)=S-(P+4)

P+4:由圖5 重定位R1處可知就是被重定位處下一條指令的虛擬地址

所以,S-(P+4)就是被重定位處下一條指令到目的地址之間的offset,完成重定位后就是相對尋址了。

由此可知推出:全局符號是基于自身符號地址尋址的,且其重定位的加數(addend)就是被重定位處的長度的負數(因為相對尋址是基于下一條指令的地址到目的地址之間的offset)。

3.2 圖2中類型2重定位記錄分析

Offset=0x2b:表明被重定位的位置在.text段中offset=0x2b處,如下圖5中的R2位置。Info=0x000700000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x07)表明重定位入口的符號在符號表中的索引是index=7。

如上圖3所示:num=7就是符號在符號表中的索引,value=0說明該符號在ndx=5(data.rel.local段)中的offset=0,size=0表明該符號占用的空間為0bytes,type=SECTION表明該符號標識一個段,bind=LOCAL表明該段是文件內部定義的,ndx=5表明該符號所在的段在段表中的索引等于5,參考上圖4的段表可知, ndx=5表示.data.rel.local段。

看到沒有,代碼段對分配在.data.rel.local段中的stat_var_gp和stat_var_sp這兩個指針進行訪問,但是尋址的方式卻不一樣,對全局變量stat_var_gp是基于自身的符號地址進行訪問,而對靜態變量stat_var_sp則基于.data.rel.local段基址進行訪問,想想看這是為什么呢?后面會詳述。

根據圖2中的Sym.name+addend=.data.rel.local+4可得:addend=4.

下面很有必要解釋下這個addend=4是怎么計算得到的。

指令相對尋址的offset是指當前執行指令的下一條指令的起始地址到目的地址之間的差值,

而重定位表中每條記錄的r_offset項(參見下面靜態鏈接關系圖)中只記錄了被重定位處在段中的offset,并沒有記錄其下一條指令的地址或重定位處的長度(因為通過重定位處長度和r_offset可以計算出下一條指令的地址),這樣一來在鏈接的時候就無法計算出它們之間相對offset,而addend就是用來彌補這個鴻溝的。

(1)全局符號

因為全局符號都是基于自己符號地址尋址的,所以addend中存儲的就是被重定位處長度的負數,上面類型1已經解釋過了。

(2)靜態符號(靜態變量和靜態指針變量)

因為靜態符號都是基于自己所在段的基地址尋址的,所以addend中存儲的就是:

被引用靜態符號在其所在段中的offset - 被重定位處的長度。

這里作如下定義,進行隨后的推導過程:

sym_section_offset : 被引用符號在其所在段中的offset

rela_position_len : 被重定位處的長度

假設符號.data.rel.local,也就是該段在可執行文件中最終的虛擬地址是S,要被修正的位置在可執行文件中的虛擬地址是P, 那么根據relocation_type=R_X86_64_PC32其修正的公式是:S+A-P。

A = sym_section_offset - rela_position_len

S + A - P = S + sym_section_offset - (P + rela_position_len)

S + sym_section_offset:就是符號stat_var_sp鏈接后的實際虛擬地址

P + rela_position_len: 就是需要被重定位處的下一條指令的虛擬地址

這樣(S + sym_section_offset) - (P + rela_position_len)就是LD合并同類項后它們之間實際的offset,將這個值更新到P位置上就完成了地址重定位操作。

因此,由圖3符號表可知stat_var_sp在.data.rel.local段中的offset=8,因此addend=8-4=4。

3.3 圖2中類型3重定位記錄的分析

Offset=0x22:表明被重定位的位置在.text段中offset=0x22處,如下圖5中的R3位置。Info=0x000300000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x03)表明重定位入口的符號在符號表中的索引是index=3。

如上圖3所示:num=3就是符號在符號表中的索引,value=0說明該符號在ndx=3(data段)中的offset=0,size=0表明該符號占用的空間為0bytes,type=SECTION表明該符號標識一個段,bind=LOCAL表明該段是文件內部定義的,ndx=3表明該符號所在的段在段表中的索引等于3,參考上圖4的段表可知, ndx=3表示.data段。

因此可知,靜態變量stat_var不是以自己的符號尋址的,而是以其所在的data作為基地址尋址的,關于這一點類型2中已經得出結論,這里是驗證。

根據圖2中Sym.name + addend = .data - 4,可得addend = -4。

根據類型2中給出的計算addend的公式:A = sym_section_offset - rela_position_len

計算過程如下:

由圖3可得符號stat_var在.data段中的sym_section_offset =0

由圖5重定位處R3可得rela_position_len=4

所以,addend = 0 - 4 = -4

3.4 圖2中類型4重定位記錄的分析

Offset=0x36:表明被重定位的位置在.text段中offset=0x36處,如下圖5中的R4位置。Info=0x001400000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x14)表明重定位入口的符號在符號表中的索引是index=20。

如上圖3所示:num=20就是符號在符號表中的索引,value=0x10說明該符號在ndx=5(data.rel.local段)中的offset=0x10,size=8表明該符號占用的空間為8bytes,type=OBJECT表明該符號標識一個對象變量,bind=GLOBAL表明該對象是全局的,ndx=5表明該符號所在的段在段表中的索引等于5,參考上圖4的段表可知, ndx=5表示.data.rel.local段。

根據圖2中Sym.name + addend = .abc_gp -4,可得addend=-4。

因為類型4和類型1都是全局指針,基于自己符號地址尋址。

所以根據類型1的結論可以推出:addend = - rela_position_len = -4

3.5 圖2中類型5重定位記錄的分析

Offset=0x41:表明被重定位的位置在.text段中offset=0x41處,如下圖5中的R5位置。Info=0x000700000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x07)表明重定位入口的符號在符號表中的索引是index=7。

如上圖3所示:num=7就是符號在符號表中的索引,value=0x00說明該符號在ndx=5(data.rel.local段)中的offset=0x00,size=0表明該符號占用的空間為0bytes,type=SECTION表明該符號標識一個段,bind=LOCAL表明該段是文件本地定義的,ndx=5表明該符號所在的段在段表中的索引等于5,參考上圖4的段表可知, ndx=5表示.data.rel.local段。

因此可知,靜態指針abc_sp不是以自己的符號地址尋址的,而是以其所在的.data.rel.local段作為基地址尋址的,關于這一點類型2中已經得出結論,這里是驗證。

根據圖2中Sym.name + addend = .data.rel.local + 14,可得addend=0x14。

根據類型2中給出的計算addend的公式:A = sym_section_offset - rela_position_len,計算過程如下:

由圖3可得符號abc_sp在.data.rel.local段中的sym_section_offset =0x18

由圖5重定位處R5可得rela_position_len=4

所以,addend = 0x18 - 4 = 0x14

3.6 圖2中類型6重定位記錄的分析

Offset=0x4c:表明被重定位的位置在.text段中offset=0x4c處,如下圖5中的R6位置。Info=0x001500000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x15)表明重定位入口的符號在符號表中的索引是index=21。

如上圖3所示:num=21就是符號在符號表中的索引,value=0x00說明該符號在ndx=7(data.rel段)中的offset=0x00,size=8表明該符號占用的空間為8bytes,type=OBJECT表明該符號標識一個對象變量,bind=GLOBAL表明該對象是全局的,ndx=7表明該符號所在的段在段表中的索引等于7,參考上圖4的段表可知, ndx=7表示.data.rel段。

根據圖2中Sym.name + addend = global_var_gp - 4,可得addend = -4。

因為類型6和類型1都是全局指針,基于自己符號地址尋址。

所以根據類型1的結論可以推出:addend = - rela_position_len = -4

3.7 圖2中類型7重定位記錄的分析

Offset=0x57:表明被重定位的位置在.text段中offset=0x57處,如下圖5中的R7位置。Info=0x000a00000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x0a)表明重定位入口的符號在符號表中的索引是index=10。

如上圖3所示:num=10就是符號在符號表中的索引,value=0x00說明該符號在ndx=7(data.rel段)中的offset=0x00,size=0表明該符號占用的空間為0bytes,type=SECTION表明該符號標識一個段,bind=LOCAL表明該段是文件本地定義的,ndx=7表明該符號所在的段在段表中的索引等于7,參考上圖4的段表可知, ndx=7表示.data.rel段。

因此可知,靜態指針global_var_sp不是以自己的符號地址尋址的,而是以其所在的.data.rel段作為基地址尋址的,關于這一點類型2中已經得出結論,這里是驗證。

根據圖2中Sym.name + addend = .data.rel + 4,可得addend=0x04。

根據類型2中給出的計算addend的公式:A = sym_section_offset - rela_position_len,計算過程如下:

由圖3可得符號global_var_sp在.data.rel段中的sym_section_offset = 0x08

由圖5重定位處R7可得rela_position_len = 4

所以,addend = 0x08 - 4 = 0x04

3.8 圖2中類型8重定位記錄的分析

Offset=0x98:表明被重定位的位置在.text段中offset=0x98處,如下圖5中的R8位置。Info=0x001600000002:由兩個部分構成(32:32),低32位(0x02)表明重定位入口的類型是R_X86_64_PC32,高32位(0x16)表明重定位入口的符號在符號表中的索引是index=22。

如上圖3所示:num=22就是符號在符號表中的索引,value=0x00且ndx=UND(undefined)說明該符號是外部符號,文件內部沒有定義,size=0且type=NOTYPE說明符號的最終類型目前還無法確定,因為外部同一個全局符號可以被定義成多個弱類型或多個弱類型+一個強類型且數據類型可以不同,所以此時無法確定(關于強弱符號的解釋請看”程序員的自我修養”一書,里面有詳解),bind=GLOBAL表明該對象是全局的,ndx=UND表明該符號是外部符號,文件內部沒有定義該符號。

根據圖2中Sym.name + addend = global_var - 4,可得addend = -4。

因為類型8是全局符號,基于自己符號地址尋址。

所以根據類型1的結論可以推出:addend = - rela_position_len = -4

3.9 圖2中類型9重定位記錄的分析

Offset=0x7c:表明被重定位的位置在.text段中offset=0x7c處,如下圖5中的R9位置。Info=0x001900000004:由兩個部分構成(32:32),低32位(0x04)表明重定位入口的類型是R_X86_64_PLT32,高32位(0x19)表明重定位入口的符號在符號表中的索引是index=25。

如上圖3所示:num=25就是符號在符號表中的索引,value=0x00且ndx=UND(undefined)說明該符號是外部符號,文件內部沒有定義,size=0且type=NOTYPE說明符號的最終類型目前還無法確定,因為外部同一個全局符號可以被定義成多個弱類型或多個弱類型+一個強類型且數據類型可以不同,所以此時無法確定(關于強弱符號的解釋請看”程序員的自我修養”一書,里面有詳解),bind=GLOBAL表明該對象是全局的,ndx=UND表明該符號是外部符號,文件內部沒有定義該符號。

根據圖6所示:relocation_type=R_X86_64_PLT32=4重定位計算公式為:L+A-P。

L: The section offset or address of the procedure linkage table entry for a symbol

LD對外部函數符號的處理分兩種情況

編譯器在處理該外部函數符號的時候是不知道這個符號是定義在普通對象還是共享對象里的,所以在rel表中統一將其定義為R_X86_64_PLT32類型,在隨后的鏈接過程會根據函數符號是定義在普通對象還是共享對象進行不同的處理。

(1)該函數符號定義在普通對象中

LD將其當做普通的R_X86_64_PC32類型進行處理,這時L+A-P = S+A-P

(2)該函數符號定義在共享對象中

LD將其作為R_X86_64_PLT32進行處理,LD會為其create一個“函數名@plt”過程和在.got.plt表中創建一個表項(用于存儲函數被加載后的實際虛擬地址),并將代碼中對該函數的訪問改為對該過程的訪問,這些操作都要在靜態鏈接的時候完成的,這個過程(函數名@plt)的地址就是L,所以relocate計算公式變為:L+A-P。

最后動態鏈接的時候會將函數的實際虛擬地址更新到.got.plt表項中,這樣該過程通過.got.plt表項就可以間接跳轉到實際要訪問的函數了。

根據圖2中Sym.name + addend = multiple - 4,可得addend = -4。

因為類型9是全局符號,基于自己符號地址尋址。

所以根據類型1的結論可以推出:addend = - rela_position_len = -4

通過對以上9種重定位類型的分析我們可以總結出如下結論:

1. 全局符號(包括全局指針)是以自己的符號地址進行重定位的

2. 靜態符號(靜態變量和靜態指針)是以自己所在的段為基地址進行重定位的

3. 指向非外部符號的指針(全局和靜態)都被分配在.data.rel.local段中

4. 指向外部符號的指針(全局和靜態)都被分配在.data.rel段中

首先要搞清楚.data.rel.local段的含義:.data表明它是一個數據段,.rel表明這個數據段中的數據是對其他符號的引用(例如int* var_p = &var),是需要被重定位的,.local表明這個被引用的符號是在本文件定義的(例如文件內部定義了int var=3),而不是外部符號(extern int var)。根據以上的解釋:.data.rel段的含義大家應該都明白了,是變量引用了外部符號,需要被重定位這里不闡述了。

注意:不管是.data.rel.local段還是.data.rel段,其內部定義的數據變量既可以是全局變量,

也可以是僅內部可見的靜態變量,這點一定要搞清楚,下面舉例說明。

例如: int a = 3; int* a_p = &a; static int* s_a_p = &a;

全局指針變量a_p和靜態指針變量s_a_p都會放在.data.rel.local段中,

但對它們的尋址是不同的:全局指針變量a_p = a_p + addend; 而靜態指針變量s_a_p = .data.rel.local + addend。

如果將int a = 3改為extern int a的話,全局指針變量a_p和靜態指針變量s_a_p都會放在.data.rel段中,對它們的尋址也會變為:全局指針變量a_p = a_p + addend; 而靜態指針變量s_a_p = .data.rel + addend。

看到這里可能會問為什么靜態符號都是以所在段的基地址為base尋址呢?

因為靜態變量(包括靜態指針變量)是文件內部定義的僅內部可見符號,所以當LD在合并同類項,建立全局符號表時是不會記錄這些僅文件內部可見的符號的,但會通過它們所在的段基地址+addend來定位它們。

例如:static int a=3; 是通過.data + addend來定位靜態變量a

static Int* a_p = &a; 是通過.data.rel.local + addend來定位靜態變量指針a_p

extern int b; static int* b_p = &b; 是通過.data.rel + addend來定位靜態變量指針b_p

通過以上的解釋大家有沒有發現一個現象,僅內部可見的符號都是以所在的段基地址為base進行尋址的,這樣做的好處就是全局符號表中只需要記錄全局符號和段符號就行了,大量的僅內部可見符號就不用記錄了。

下面列舉重定位表rela.data.rel.local中的兩條記錄來分析如何對數據區內的指針變量進行重定位。

首相要明白指針是對一個符號的絕對地址引用,所以relocation_type=R_X86_64_64(絕對地址引用)。

3.10 圖2中類型10重定位記錄的分析

Offset=0x08:表明被重定位的位置在.data.rel.local段中offset=0x08處,如下圖5中的R10位置。Info=0x000300000001:由兩個部分構成(32:32),低32位(0x01)表明重定位入口的類型是R_X86_64_64,高32位(0x03)表明重定位入口的符號在符號表中的索引是index=0x03=3。

如上圖3所示:num=3就是符號在符號表中的索引,value=0x00說明該符號在ndx=3(data段)中的offset=0x00,size=0表明該符號占用的空間為0bytes,type=SECTION表明該符號標識一個段,bind=LOCAL表明該段是文件本地定義的,ndx=3表明該符號所在的段在段表中的索引等于3,參考上圖4的段表可知, ndx=3表示.data段。

因此可知,靜態變量stat_var_bk不是以自己的符號地址尋址的,而是以其所在的.data段作為基地址尋址的。

根據圖2中Sym.name + addend = .data + 4,可得addend=0x04。

由于relocation_type=R_X86_64_64所以根據圖6得知其計算公式為:S+A

S:是符號鏈接后的虛擬地址

A:加數(修正值)

stat_var_bk是以.data段作為基地址尋址的,由下圖5可知stat_var_bk在.data段內的offset=4

所以,符號stat_var_bk的虛擬地址=.data連接后的虛擬地址+offset=S+offset。

所以可以推出:addend=符號在所在段的offset.

3.11 圖2中類型11重定位記錄的分析

Offset=0x10:表明被重定位的位置在.data.rel.local段中offset=0x10處,如下圖5中的R11位置。Info=0x001300000001:由兩個部分構成(32:32),低32位(0x01)表明重定位入口的類型是R_X86_64_64,高32位(0x13)表明重定位入口的符號在符號表中的索引是index=19。

如上圖3所示:num=19就是符號在符號表中的索引,value=0x08說明該符號在ndx=3(data段)中的offset=0x08,size=4表明該符號占用的空間為4bytes,type=OBJECT表明該符號標識一個對象,bind=GLOBAL表明綁定的是全局對象,ndx=3表明該符號所在的段在段表中的索引等于3,參考上圖4的段表可知, ndx=3表示.data段。

因此可知,全局變量abc是以自己的符號地址尋址的。

根據圖2中Sym.name + addend = .data + 0,可得addend=0x00。

由于relocation_type=R_X86_64_64所以根據圖6得知其計算公式為:S+A。

S就是全局變量abc的虛擬地址,所以可以推出:addend = 0x00。

由類型10和11可以得出如下結論:

指針的重定位操作分為兩種類型:對靜態變量引用和對全局變量引用

(1)對靜態變量引用

公式S+A中的S是段基址,那么A就是符號在該段中的offset

(2)對全局變量的引用

公式S+A中的S就是實際符號地址,那么A值永遠為0。

下面看一下main.c依賴的外部模塊funcs.c的對象文件及相關的重定位表。

從圖1中funcs.c的定義可以看出,函數set_multiple_index和函數multiple是定義在同一個文件中的,但是從下面圖7 中funcs.o的重定位表可以發現multiple對set_multiple_index函數的調用是需要重定位的,將set_multiple_index當作外部符號處理了。

重定位類型是:R_X86_64_PLT32,參考上面的main.c中對multiple調用的處理,這里就不多做解釋了。

編譯時對調用靜態方法的處理與對全局方法的處理是不同的

由圖5中對divide方法的調用可以看出,在編譯時就通過相對尋址設置好了跳轉地址,在圖2的重定位表中也沒發現要對divide函數調用進行重定位,所以靜態方法調用是不需要重定位的,因為首先它肯定是在文件內有定義的,其次調用它的代碼塊肯定與靜態方法編譯在同一個.text段中的,因此它們之間的offset就固定下來了,即使LD合并很多.text段在一起形成一個segment,它們之間的offset也不會改變,因此可以在編譯時就設定好。

下圖11是main.c鏈接成可執行文件后main的匯編代碼,從中可以看出對divide調用的offset值與圖5 main.o中的offset是一樣的。

第四部分 動態鏈接微觀詳解

4.1 動態鏈接的主要有2大優點

1. 共享DSO,節省內存

2. DSO版本更新后,程序不需要重新編譯

要實現以上兩點,DSO必須是PIC代碼。

4.2 動態鏈接有1個缺點

在運行時要一次性鏈接整個程序依賴的包(DSO如果已經存在于內存的話,僅需要relocate and remapping),這樣程序運行的速度肯定會慢的(所以比靜態共享庫要慢,后面會講靜態共享庫的優缺點)。尤其是程序運行的過程中,有可能會走不同的分支,從而運行不到有些依賴包,這時也把這些包鏈接和加載進內存會大大消耗時間和內存。

為了克服這個缺點,就有了PLT技術,按需加載,用到了才加載并鏈接。

但有一點一定要注意:如果對DSO包定義的全局變量有引用的話,那么不管該變量的引用是否會被執行到,該DSO包都會被加載到內存并對該全局變量的引用重定位,因為PLT是對函數調用的延遲加載技術,而全局變量訪問是沒有這項技術的,所以函數可以是RTLD_LAZY,但變量一定要RTLD_NOW。

4.3 動態鏈接庫/對象為什么要是PIC code

(1)共享對象(dso)要想被不同進程共享必須是PIC code

想想看如果dso包不是PIC code的情形

如果sample.dso包被加載到進程A的0x3000~0x4000虛擬地址空間,那么該包中所有對絕對虛擬地址的引用(指令部分)都要以0x3000作為virt_base進行更新;當進程B要把該包加載進自己0x7000~0x8000虛擬地址空間,那么就無法共享進程A加載的sample.dso包的指令部分了。

(2)DSO包的更新與發布必須要PIC code

下面以靜態共享庫為例說明。

靜態共享庫的虛擬基地址是固定的,所以每個進程要想引用靜態共享庫,那么在每個進程的虛擬地址空間中,必須預留固定的虛擬地址區間用于該靜態共享庫。

例如sample.sso是靜態共享庫且其虛擬基地址是0x4000,占用一頁大小(0x1000),這樣每個要用到它的進程都要在自己的進程地址空間中預留0x4000~0x5000這個虛擬地址區間給sample.sso。

程序在靜態鏈接的時候就可以完成所有對靜態共享庫的函數和全局變量引用的重定位工作(靜態共享庫在進程中的虛擬地址是固定的),但是不會在此時把靜態共享庫鏈接到應用程序中,而是等到運行時才加載到內存并映射到程序固定的虛擬地址區間,通過這樣的方式實現庫文件在不同進程之間的共享,這樣只保留一份庫文件在內存中,從而實現節省內存和減少程序本身大小的問題,而且運行時僅需加載一次但不需要重定位了,所以速度比DSO要快。

但這樣的方式實現庫共享會導致以下兩個大問題:

(1)進程虛擬地址的利用很不靈活很容易造成地址沖突

(2)靜態庫文件如果版本升級后其中的函數或全局變量的地址一旦發生改變,原來的應用程序必須重新鏈接。

所以PIC code就是為了解決問題1,裝載時重定位就是為了解決問題2,這樣靜態共享庫就編程動態共享庫了。

4.4 編譯和鏈接的總特點

1.4.1 編譯器在編譯的時候(注意不是鏈接),對象內對所有全局變量(包括內外定義或聲明的)的引用,不管是聲明的還是定義的,默認都是當作外部符號處理的,都會放到重定位表中去。

1.4.2 連接器在鏈接可執行文件的時候,會遍歷程序所有的符號表(包括依賴的共享庫),并做如下的操作。

(1)首先檢查主程序定義的全局強符號與所有共享庫的全局強符號是否有重定義沖突。

(2)檢查主程序及共享庫中所有以extern聲明的全局符號,在其他的模塊和共享庫中是否有定義,如果沒有定義就會報符號未定義的鏈接錯誤。

(3)如果主程序中聲明的extern全局符號在其它的模塊中定義了,那么在靜態鏈接的時候就直接重定位了(鏈接時已經知道其虛擬地址了)。

(4)如果主程序中聲明的extern全局符號在其它的共享庫中定義了,那么此時還無法決定該符號的實際虛擬地址是多少,那么就根據主程序是否是PIC code進行不同的處理,后面會詳解。

(5)在鏈接共享庫自身的時候,是不會檢查共享庫中用到的但聲明為extern 的全局符號是否有定義,但當將共享庫鏈接到主程序的時候,連接器會檢查共享庫用到的但聲明為extern 的全局符號在全局符號表中是否有定義。

例如在sample.dso共享庫中聲明了一個extern int ext_var; 并且被其定義的funcA使用了;而Test.c主程序依賴該sample.dso庫,并且調用了其定義的方法funcB(沒有使用ext_var),即使這樣在鏈接Test.c為可執行程序的時候也會報undefined reference to ext_var這樣的錯誤,如果在Test.c中或其他依賴的庫文件中定義了該變量就不報錯了。

4.5 對extern變量和弱類型變量的處理是動態鏈接中比較復雜的部分。

下面從編譯和鏈接兩個階段分別講解

4.5.1編譯階段

1. extern 變量被當做外部符號(ndx=UND)處理,如果代碼和數據有對其引用,重定位表中有R_X86_64_PC32類型的重定位記錄。

2. 弱類型變量(例如這樣: int a;僅聲明未定義的變量)會被當做COMMON類型的符號(ndx=COM)來處理,如果代碼和數據有對其引用,重定位表中有R_X86_64_PC32類型的重定位記錄。

COM類型在編譯的時候其實也可以被看作是UND類型,因為它不屬于當前文件的任何一個段,之所以將其定義為COM類型,那是因為后面LD的時候有用。

4.5.2 鏈接階段

根據ndx=UND和ndx=COM的區別,分開講解

4.5.2.1 ndx=UND

說明該變量是extern聲明的外部變量,在本文件中沒有定義,所以不屬

于當前文件的任何一個段,對它的處理分兩種情況。

1. 共享對象中聲明的extern變量

因為共享對象中,對(內外)全局符號(函數和變量)的訪問,都被當做外部符號來處理,所以extern聲明的變量當然也就按照外部符號處理了,最終鏈接成的共享對象的.rela.dyn段中,會有一條R_X86_64_GLOB_DAT類型的重定位記錄。

2. 可執行文件中聲明的extern變量

這里又分成兩種情況

(1)該extern變量是定義在主程序的其它模塊文件中,那么就按照靜態鏈接來處理,在鏈接成主程序的過程中,就重定位好了,相對尋址即可。

(2)該extern變量是定義在DSO對象中的話,那么又可以分成兩種情況來處理。

【1】如果可執行文件不是PIC code的話

那么必須要在.bss段中分配一個該變量的副本,然后重定位到該處即可,在加載DSO的過程中,會將DSO中的該變量的初始值COPY到可執行程序.bss段中的這個變量的副本上,然后會重定位DSO中該變量對應的.got表項,使其指向主程序.bss段中的這個副本。

可能有人會問,為什么不能在主程序的.got中分配一個表項,然后重定位到共享對象中定義的該變量,這樣多簡單方便,因為主程序對共享對象中定義的函數引用就是這么干的(通過.got.plt表項);稍后會做詳細解讀,這里可能大多數人都沒搞懂,而且俞甲子的那本書說的也不對,主要原因是:指令對數據訪問地址無關性的處理和對函數調用地址無關性的處理機制不一樣。

【2】如果可執行文件是PIC code的話

通過在.got表中分配一個表項,用于存儲DSO中定義的該變量加載后的實際虛擬地址,實現重定位。

4.5.2.2 ndx=COM

鏈接階段對ndx=COM的處理也分為兩種情況。

1. DSO中聲明的弱類型變量

鏈接的時候會在最終的DSO對象的.bss段中為其分配空間,然后.rala.dyn段中會有一條R_X86_64_GLOB_DAT類型的重定位記錄。

2. 主程序中聲明的弱類型變量

有3種情況需要考慮

(1)主程序所依賴的DSO中沒有該變量的聲明或定義。

會在主程序的.bss段中為其分配空間,然后根據重定位記錄重定位。

(2)主程序所依賴的DSO中也僅有該變量的聲明(弱類型)。

會在主程序的.bss段中為其分配空間,然后根據重定位記錄重定位。

(3)主程序所依賴的DSO中有該變量的定義(強類型,以它為準)。

這又得分成兩種情況來看

【1】主程序不是PIC code

這時就相當于對DSO中該變量的引用了,所以會在.bss中分配一個副本,且在.rela.dyn段中會新增有一條R_X86_64_COPY類型的重定位記錄,后面會詳解。

【2】主程序是PIC code

這時就相當于對DSO中該變量的引用了,但不會在.bss中分配一個副本,而是會在.got表中增加一個指向該變量的表項,并在.rela.dyn段中會新增有一條R_X86_64_GLOB_DAT類型的重定位記錄,后面會詳解。

4.6 舉例詳解以上列舉的動態鏈接過程中的各類重定位類型

下面是主程序和兩個DSO的源碼

4.6.1 分析funcs.c的編譯和鏈接

因為maths.c是一個純粹的函數庫,沒有需要重定位的引用,所以這里先分析funcs.c的編譯和鏈接,看看DSO庫或對象是如何生成的。

如圖15所示:通過gcc -fPIC -c funcs.c -o funcs.o命令生成PIC目標對象。

由上圖15所示:

所有對全局變量的訪問(不管是弱類型還是強類型,或是extern類型)都會有相應的R_X86_64_REX_GOTP類型重定位記錄,該類型告訴鏈接器要創建.got表并且將所有對全局變量的訪問都修改為通過.got中的表項間接訪問,從而實現指令訪問全局變量的地址無關性。

所有對全局函數的訪問都會有相應的R_X86_64_PLT32類型重定位記錄,該類型告訴鏈接器要建立.got.plt表并且將所有對全局函數的訪問都修改為通過.got.plt中的表項間接訪問,從而實現指令訪問全局函數的地址無關性。

圖15中重定位表.rela.text中的各個字段的含義這里就不一一解釋了,因為上一章的靜態鏈接詳解中已經詳細介紹過了,這里僅對weak類型變量的Sym.value值解釋一下;因為weak變量的編譯后的ndx=COM不在任何段中,所以其Sym.value值表示該變量的長度,而不再是符號在段中的offset了,這點一定要清楚,這是ELF協議規定的。

下圖17 funcs.o的符號表中,高亮部分顯示了weak_var1和weak_var2的ndx=COM,weak_var3在本文件中確實有定義,所以其ndx=3(.data段);set_multiple_index是在本文件定義的,所以其ndx=1(.text段),而math_add不是本文件定義的,所以其ndx=UND。

下面看看執行完鏈接指令ld -fPIC -shared -o funcs.dso funcs.o 后得到的動態共享對象,其重定位表有哪些變化。

下面通過funcs.dso的符號表,段表,數據段表和代碼段表詳細闡述DSO對象在加載的時候是如何被重定位的。

如圖19所示,weak_var1,weak_var2和weak_var3的ndx分別為14,14和13;再看看段表圖20可知ndx=14是.bss段,ndx=13是.data段。

所以對于弱類型在鏈接DSO的過程中,最終會在.bss段為其分配內存空間的。

首先得詳細介紹下如何利用PLT技術實現延遲或按需綁定外部函數。

如果不使用PLT技術的話,程序啟動后,動態鏈接器會將程序中所有要訪問的外部符號的實際虛擬地址更新到對應的.got表項中(當然先要加載對應的DSO),程序的指令部分是通過.got表項間接尋址這些外部符號的,從而實現了指令的PIC。

而PLT(procedure linkage table)相當于在指令間接尋址和.got表之間又加了一層間接尋址,它是一段代碼,這點一定要搞清楚。

因為PLT是專門用于對外部函數引用的延遲綁定,所以將原先的一個.got表分成了兩個表:

.got表: 專門用于存儲外部全局變量的實際虛擬地址

.got.plt表:專門用于存儲外部全局函數的實際虛擬地址

.got.plt表的結構稍微有點特殊,它的前三項分別被用于存儲:.dynamic段地址,本模塊ID和dl_runtime_resolve的地址;之后的表項才被用于存儲每個外部函數的實際虛擬地址。

.plt段:是個代碼段,專門用于將被調用到的外部全局函數的實際虛擬地址綁定到對應的.got.plt表中。

它的結構圖22的高亮注釋對其進行了詳細解釋,下面通過對math_add的綁定再次進行詳細解讀。

math_add是funcs.dso的外部函數,因此在funcs.dso中有一個math_add@plt項,圖22中的0x108d處對math_add的調用就變成先跳轉到math_add@plt(相對跳轉),而不是直接通過.got表項間接跳轉到外部函數。

下面看看math_add@plt項干了什么。

如代碼圖22中0x1020處所示:

第一條指令間接跳轉到.got.plt表項0x4020處存儲的地址,由圖21可得0x4020處存儲的地址是0x1026,而0x1026就是math_add@plt的下一條指令地址,該指令將math_add符號引用在重定位表.rela.plt中的下標入棧,第三條指令是跳轉到0x1000處。

0x1000處的.plt項是每個plt函數項的都要執行的部分,因此將其獨立出來單獨成項。

.plt項第一條指令是將本模塊的moduleID入棧,然后跳轉到dl_runtime_resolve函數執行math_add的綁定工作,dl_runtime_resolve函數會根據外部函數引用在.rela.plt重定位表中的索引(入棧的參數),解析處需要的重定位信息,查找全局重定位表,找到math_add符號的實際虛擬地址,并根據重定位類型R_X86_64_JUMP_SLO = S(符號實際虛擬地址),將math_add的實際虛擬地址直接更新到.got.plt表項0x4020處,最后再返回繼續執行call math_add@plt,就跳轉到maths.dso的math_add執行了。

以上詳述了如何通過PLT技術延遲綁定外部函數引用,下面將講一下如何綁定外部變量的引用。

由圖18可知,動態鏈接的重定位表被分為兩類:.rela.dyn和.rela.plt。

.rela.dyn : 用于對外部變量引用的重定位

.rela.plt : 用于對外部函數的重定位

我們看一下圖18中.rela.dyn表的第一條記錄是怎么重定位的。

Offset Info Type Sym. Value Sym. Name + Addend

000000003fe0 000400000006 R_X86_64_GLOB_DAT 0000000000004034 multiple_index + 0

Offset=0x3fe0是.got表的起始位置,說明此處用于存儲multiple_index的實際虛擬地址。

Info.relocation_type=0x06=R_X86_64_GLOB_DAT = S表明就是將multiple_index的實際虛擬地址更新到0x3fe0即可。

Info.symbol_index=0x04說明該符號在.dynsym動態符號表中的索引是4,看一下圖18可知multiple_index是分配在.data段中的全局對象。

此時的Sys.value是鏈接DSO對象時分配的虛擬地址0x4034(以0x00為基地址),圖21中顯示此處屬于.data段且存儲的值為0x03,在動態鏈接的時候會將funcs.dso被加載到進程的虛擬基地址+Sys.value形成最終的實際虛擬地址并更新到全局符號表中,隨后會遍歷.rela.dyn重定位表并檢索全局符號表中multiple_index的實際虛擬地址,最后更新到對應的.got表項,到此就完成了對外部變量引用的重定位了。

這里可能大家會有個疑問,為什么對外部變量引用不應用PLT技術進行延遲綁定呢?

這里解釋一下,一方面是因為DSO對象之間相互引用全局變量這種情況本身就是要盡量避免的,因為這會增加共享模塊之間的耦合度,不利于功能擴展,所以需要被重定位的量相對于函數來說比較少,沒必要延遲綁定;另一方面就是從技術實現上來說在鏈接階段也不可能,除非在編譯階段就做好了這方面的考慮,后面會通過解釋主程序(非PIC)引用共享對象中的變量時會在自己的.bss段中分配一個COPY對象的原因來回答這個問題。

4.6.2 下面詳細分析主程序被編譯和鏈接成PIC和非PIC code時,對共享對象中函數和變量的引用是如何處理的。

4.6.2.1 主程序是PIC code

用命令:gcc -fPIC -c -main.c -o main.pic.o將main.c編譯成PIC code模式,重定位表如下圖23所示:

這里和圖15 funcs.o的重定位表的重定位類型一樣,分為兩類:

(1)所有對全局變量的訪問(不管是弱類型還是強類型,或是extern類型)都會有相應的R_X86_64_REX_GOTP類型重定位記錄,該類型告訴鏈接器要創建.got表并且所有對全局變量(內外定義或聲明的)的訪問都修改為通過.got中的表項間接訪問,從而實現指令訪問全局變量的地址無關性。

(2)所有對全局函數的訪問都會有相應的R_X86_64_PLT32類型重定位記錄,該類型告訴鏈接器要建立.got.plt表并且將所有對全局函數的訪問都修改為通過.got.plt中的表項間接訪問,從而實現指令訪問全局函數的地址無關性。

下圖24是對main.pic.o的部分反匯編,這里主要關注如何實現訪問全局變量和全局函數的地址無關性。

圖24中注釋,詳細分析了如何實現對全局函數inner_add和全局變量dso_var引用的PIC。

對inner_add引用的分析

地址0x22處的指令,操作碼E8表明它是call指令相對跳轉,后面的4個字節是offset(需要被重定位),由圖23 重定位表可知,此處是對inner_add引用的重定位。

對該處offset的重定位分兩種情況:

1. 如果inner_add是主程序內部自定義的,那么靜態鏈接的時候就可以計算出inner_add相對于調用指令之間的offset了,因此靜態鏈接的時候就實現重定位了。

2. 如果inner_add是外部DSO中定義的,那么靜態鏈接的時候無法知道其虛擬地址,所以就得通過.got表項實現運行時動態鏈接重定位。

注意:可執行程序也是通過在外部函數引用與.got表之間增加一層間跳轉(函數名@plt過程),實現外部函數引用的PIC,但是不分.got和.got.plt兩個表了,統一都放在.got表中,.got的前三項分別用來存儲: .dynamic段地址,本某塊moduleID和dl_runtime_resolve地址;而且主程序中對外部函數的引用是立即重定位(盡管指令也是通過call 函數名@plt調用,但不起作用)和外部變量引用的處理是一樣,下面會debug驗證這一點。

下面看看main.pic.o被鏈接成可執行文件后的重定位表和反匯編代碼是什么樣

用gcc -o main.pic main.pic.o ./funcs.dso ./maths.dso生成main.pic可執行文件

由上圖25可知,只有對外部變量dso_var,內部聲明weak_var3和外部函數multiple的引用需要重定位,而之前main.pic.o的重定位表(圖23)列出的對inner_add,weak_var1和weak_var2引用的是需要重定位的,這里沒有了。

下面結合main.pic的反匯編代碼段和數據段詳細分析為什么這三個符號不需要重定位了。

PIC主程序中對自定義的全局函數inner_add的處理

圖26中0x1167地址處是對inner_add引用的重定位,是直接修正為相對跳轉到inner_add(0x1145),從中可以看出兩點:

1. 沒有在.plt段中創建一個inner_add@plt過程用于間接訪問和重定位.got中其對應的表項。

這和鏈接器處理共享對象中自定義的函數之間引用不一樣。

從圖15(目標對象funcs.o)和圖18(目標對象main.pic.o)中可以看出,編譯階段它們對內部自定義的set_multiple_index和inner_add的處理,用的都是一樣的重定位類型:R_X86_64_PLT32。

但鏈接階段不一樣,共享對象中即使函數都是定義在同一個DSO中,但是它們之間的調用還是通過.got.plt表間接跳轉的。

可以看一下funcs.dso中的重定位表(圖18).rela.plt,可以看出multiple和set_multiple_index這兩個全局函數雖然都是定義在funcs.dso中的,但是multiple調用set_multiple_index函數還是要通過.got.plt間接調用。

2. 沒有在.got表中創建一個表項,用于存儲inner_add實際虛擬地址。

這個就好明白了,指令(e8)是相對跳轉指令且沒有在.plt段中創建間接跳轉項(inner_add@plt),是在靜態鏈接的時候就修正好了,所以不會在.got表中創建表項了。

PIC主程序對自己聲明的弱類型全局變量的處理

這里主要分為2種情況

情況1: 主程序和依賴共享對象中都聲明了同名的弱類型

weak_var1和weak_var2在main.c和funcs.c中都聲明為弱類型

那么會在主程序的.bss段為weak_var1和weak_var2分配空間,這里不管是int還是long數據類型都分配了8bytes,如圖26 main.pic的數據部分所示。

我們知道目標對象funcs.o的重定位表(圖15)顯示,weak_var1和weak_var2的重定位類型都是R_X86_64_REX_GOTP,這是指示LD在鏈接的時候在.got表中創建相應的表項并通過.got表項間接訪問weak_var1和weak_var2。

共享對象funcs.dso的重定位表(圖18)顯示,weak_var1和weak_var2的重定位類型都是R_X86_64_GLOB_DAT,表明LD在鏈接的時候確實為它們在.got中創建表項了并通過got表項間接訪問。雖然已經在(圖21)funcs.dso的.bss段中為它們分配了空間了嗎,但是此時還不知道其他的模塊(例如主程序)或DSO中是否定義了同名的強類型,如果有定義了強類型這里就要重定位到強類型的地址了,如果沒有就重定位到自己的.bss段中分配的變量。

目標對象main.pic.o中的重定位表(圖23)顯示,weak_var1和weak_var2的重定位類型都是R_X86_64_REX_GOTP,這也是指示LD在鏈接的時候在.got表中創建相應的表項并通過.got表項間接訪問weak_var1和weak_var2;但因為main.pic.o是主程序,所以在鏈接為可執行程序的時候,會檢索自己及其依賴的所有對象的符號表,從而能夠確認這兩個符號沒有請類型定義,所以就會將分配在自己.bss段中的weak_var1和weak_var2在符號表中設定為強類型符號,然后對這兩個變量的引用也不經過.got表了,其依賴對象funcs.dso中對這兩個變量的訪問也都會重定位到main的.bss段中來(因為main中的被改為強類型了)。

這里要著重解釋一下這兩個變量的重定位方式是如何轉變的。

我們知道目標對象main.pic.o中已經將對weak_var1和weak_var2的訪問分成兩步來完成了,如圖24(main.pic.o代碼部分)的0x62和0x69兩處地址所示:

第一步:48 8b 05 00 00 00 00 mov 0x0(%rip),%rax

寄存器相對尋址從.got表項中取出weak_var1的實際虛擬地址存入%rax。

第二步:8b 10 mov (%rax),%edx

寄存器尋址讀取weak_var1的值

現在主程序中可以通過寄存器相對尋址(也就是第一步)一步就訪問到存儲在main.pic的.bss段中的weak_var1了,但現在這里已經被編譯成分兩步訪問了,怎么辦?

有兩個辦法:

【1】不用修改第一步指令

LD的時候還是為weak_var1在.got中創建表項,并在rela.dyn中創建該變量重定位記錄,這樣在運行main主程序的時候動態鏈接器會將main運行時bss段中weak_var1的實際虛擬地址更新到其對應的.got表項中了,從而完成重定位。這也就和DSO對象的處理方式一樣了。

【2】修改第一步指令

通過lea指令和(%rip)相對尋址相結合的方式,實現運行時動態獲取bss段中weak_var1的實際虛擬地址;但這種方式有個前提:不能改變指令的長度。

目前LD就是采用了這種方式將第一步改為:

圖27中0x1127處:48 8d 05 6a 2e 00 00 lea 0x2e6a(%rip),%rax

指令長度都為7bytes,沒有變。

為什么指令長度不能變呢,如果能變的話就簡單了,直接將第二部去掉不就行了嗎,通過寄存器相對尋址一步就搞定了,想想看為什么?

因為代碼段中如果存在大量相對跳轉指令的話,一旦你增加或減少了指令的長度,很可能會導致這些跳轉目的地址是錯誤的了,想想看如果有大量判斷語句以weak_var1為判斷條件進行跳轉,尤其是通過標號的跳轉,這些跳轉都是相對尋址的,別指望LD會幫你對這些進行修改,不可能的,LD這時壓根不知道上下文的聯系和邏輯。

這才是非PIC主程序如果引用DSO中的變量的時候,靜態LD要在自己的bss段中分配一個該變量的COPY的根本原因。

因為在用非PIC方式編譯主程序時,對全局變量的訪問就是用一條寄存器相對尋址的(也就是上面的第一步)搞定,在鏈接的時候其實是知道這個變量是被主模塊定義的還是被DSO對象定義的,但是代碼的長度已經固定了,不能改了。

如果是主模塊自己定義的話,那么靜態LD的時候直接重定位就好了。

如果是DSO對象中定義的話,那么LD的時候絕不能改為在該寄存器相對尋址指令(上面的第一步)的后面再加一條寄存器尋址指令,在GOT表中增加一項,最終通過got表間接尋址的方式尋址。

所以LD最終采用了這個方式:在主模塊的bss段中為該變量分配一個副本,然后直接重定位到這個副本。

情況2: 主程序中定義了同名弱類型,而依賴對象中定義了同名強類型

因為主程序是使用PIC方式編譯的,所以對所有全局變量(內外)的訪問在編譯時就確定通過如上所述的兩步訪問模式尋址的。

weak_var3是DSO中定義的全局變量,在鏈接器鏈接主程序的時候,會檢索自己包括依賴對象的符號表的,從而可以確定是對funcs.dso中定義的該變量的引用,所以是不會在自己的bss段再為其分配空間了,接著會在.got中為其分配一個表項,將第一步寄存器相對尋址重定位到該表項,這樣在程序運行并通過動態鏈接器加載funcs.dso并且將weak_var3的實際虛擬地址更新到對應的got后,如上所述的第一和第二步就可以通過got表間接訪問weak_var3了。

4.6.2.2 主程序不是PIC code

下面通過分析非PIC主程序的重定位表,反匯編代碼來看看與其PIC code的區別。

通過gcc -c -o main.o main.c 編譯出非PIC main.o

如圖28所示,在非PIC模式編譯時,對全局變量和方法(內外)都是當做外部符號處理的,都是需要重定位的,不過重定位類型和PIC模式編譯的不一樣。

(1)方法的重定位類型都是:R_X86_64_PLT32。

這里著重講一下,為什么方法在PIC和非PIC模式的編譯下都可以是R_X86_64_PLT32呢?

通過上面的講解我們知道,在編譯時,對全局變量的訪問會根據PIC和非PIC模式生成的指令個數是不一樣的,PIC模式會生成兩條指令,非PIC模式會生成一條指令,所以LD的時候考慮的情況就比較多。

而方法的引用在兩種編譯模式下就是一條相對跳轉指令搞定,這是因為如下原因:

1. 如果引用的方法是在主模塊中定義的,那么在靜態鏈接的時候就知道它們之間的offset了,所以靜態LD時通過計算出來的offset,直接修改該指令的操作數就完成重定位了。

2. 如果引用的方式DSO中定義的,那么在靜態鏈接的時候還不知道方法的實際虛擬地址,所以在靜態鏈接的時候,會先創建.plt段及函數名@plt表項并且重定位call指令使其相對跳轉到該表項,然后再在.got表中分配一個該方法的表項并且表項中存儲函數名@plt過程的第二條指令的地址,從而實現通過.got.plt表對方法引用的延遲綁定。

(2)全局變量的重定位類型都是R_X86_64_PC32

這種類型的重定位公式:S+A-P,就不解釋了,靜態鏈接那章已經詳細解釋過了,總是相對尋址形式。

下面看一下鏈接后可執行程序的重定位表有什么變化。

通過 gcc -o main main.o ./funcs.dso ./maths.dso 生成main可執行文件。

下面結合圖29著重分析R_X86_64_COPY類型的作用。

.rela.dyn重定位表中顯示:對weak_var3和dso_var的引用的重定位類型變成R_X86_64_COPY類型了,上面已經講過非PIC程序對全局變量的訪問一律都是用一條寄存器相對尋址指令實現的,而此時外部定義的變量weak_var3和dso_var的實際虛擬地址還不知道呢,所以就在主程序的.bss段中分配一個weak_var3_copy和dso_var_copy,并將指令修正到指向這些副本,當主程序運行時,會處理重定位表.rela.dyn中的所有重定位記錄(變量的重定位是沒有延遲綁定的),首先會加載funcs.dso,這時內存中就有兩個weak_var3和兩個dso_var了,此時

動態鏈接器會將funcs.dso中這連個變量的值copy到副本中,實現數據的一致性。

當主程序執行到multiple方法時,動態鏈接器就會根據funcs.dso中的重定位記錄,將funcs.dso中的.got表中用于存儲weak_var3和dso_var實際虛擬地址的表項,重置為存儲主程序.bss中分配的 weak_var3_copy和dso_var_copy的實際虛擬地址,至此分別完成了主程序中副本數據的初始化和funcs.dso中對這兩個變量引用的重定位,而funcs.dso中自定義的這兩個全局變量就廢棄了。

可能有人會問,可以對寄存器相對尋址這一條訪存指令,建立一個重定位記錄,這樣當把funcs.dso加載到進程的地址空間后,就知道weak_var3或dso_var的實際虛擬地址了,然后計算該訪存指令到weak_var3或dso_var的實際虛擬地址之間的offset,然后重定位該指令的操作數,這樣不就可以了嗎?

的確是可以這樣,但相對尋址要修正的操作數的長度是4bytes,也就是最大4G的尋址空間,而對于64位處理器來說,當funcs.dso映射到進程的虛擬地址基地址到調用指令之間的距離一旦大于4G,那么就無法訪問了,所以這種方式有缺陷。

下面非PIC主程序的數據段和代碼段很好的印證了以上的分析

好了羅里吧嗦這么多終于解釋完了動態鏈接的各種細節。下面就通過debug PIC模式的main程序來驗證一下是否如上所說。

因為加入了debug信息,所以有些地方的offset就不一樣了,所以要重新對main.pic和funcs.dso進行反匯編。

結果如下圖所示:

驗證點1: 主程序中的PLT延遲加載是否有效

根據以上說的,只有調用了funcs.dso中的multiple方法才會綁定,我們看看是不是這樣。

因為inner_add方法是在multiple方法之前就調用了,所在調用inner_add處打斷點,

看看此時有沒有綁定multiple。

圖34

如上圖34可知,multiple@plt過程的實際虛擬地址是0x555555555030,根據圖32 PIC-main-debug的反匯編代碼片段,如下所示:

0000000000001030 <multiple@plt>:

1030: ff 25 82 2f 00 00 jmpq *0x2f82(%rip) # 3fb8 <multiple>

1036: 68 00 00 00 00 pushq $0x0

103b: e9 e0 ff ff ff jmpq 1020 <.plt>

0x555555555030 + 0x6 + 0x2f82 = 0x555555557FB8,該值就是.got表(主程序只有一個.got表,沒有.got.plt表)中的一個表項的虛擬地址,該表項中存儲multiple函數實際虛擬地址,這里用“函數名@got.item”這種形式來表示該函數引用對應的got表項的虛擬地址,既multiple@got.item = 0x555555557FB8。

由上圖34可知:

1. 通過“x /2wx 0x555555557FB8”指令打印出該處內存存儲的值為0x7ffff7fc8190,這就是multiple函數的實際虛擬地址,即*(multiple@got.item) = 0x7ffff7fc8190。

2. 通過p multiple指令打印出來的該函數的虛擬地址也是0x7ffff7fc8190。

3. 通過“x /2wx 0x555555557FA0”指令打印出.got表起始處存儲的.dynamic的地址為0x3d90,由圖32可知.dynamic段的地址的確是0x3d90,.got段的起始處存儲的也是0x3d90。

因為,此時還沒訪問mulitple函數呢,這1,2的值竟然是一樣的,所以對于主程序來說,PLT壓根就沒起作用。

驗證點2:funcs.dso調用math_add的時候,PLT延遲綁定有沒有起作用。

由funcs.c的源碼可以知道,math_add是被set_multiple_index調用的,所在調用set_multiple_index處打斷點,按道理是不應該會觸發對math_add的綁定的。

圖35

如上圖35可知,math_add@plt過程的實際虛擬地址是0x7ffff7fc8040,根據圖33 funcs.dso-debug的反匯編代碼片段,如下所示:

0000000000001040 <math_add@plt>:

1040: ff 25 da 2f 00 00 jmpq *0x2fda(%rip) # 4020 <math_add>

1046: 68 01 00 00 00 pushq $0x1

104b: e9 d0 ff ff ff jmpq 1020 <.plt>

0x7ffff7fc8040 + 0x6 + 0x2fda = 0x7FFFF7FCB020, 該值就是.got.plt表中的一個表項的虛擬地址,該表項中存儲multiple函數的實際虛擬地址,這里用“函數名@got.plt.item”這種形式來表示該函數引用對應的got表項的虛擬地址,既math_add@got.plt.item = 0x555555557FB8。

由上圖35可知:

1. 通過“x /2wx 0x7FFFF7FCB020”指令打印出該處內存存儲的值是0x7ffff7fc8046,也就是函數math_add的實際虛擬地址,即*(math_add@got.plt.item) = 0x7ffff7fc8046。

2. 通過p multiple指令打印出來的該函數的虛擬地址也是0x7ffff7fc30f5。

3. 通過“x /2wx 0x7FFFF7FCB000”指令打印出.got.plt表起始處存儲的.dynamic的地址為0x3e40,從圖33可知,.dynamic段的地址的確是0x3e40,.got.plt的起始項存儲的也是0x3e40。

因為,此時還沒訪問mulitple函數,1,2這兩處的值也不一樣,所以funcs.dso對maths.dso中math_add方法的引用是延遲綁定的。

之前還講過,在沒有重定位之前,.got.plt中每個表項(不包括前三項)存儲的默認值是其對應的“函數名@plt”過程的第二條指令的地址,從圖33可知,0x4020處存儲的地址是0x1046,正是math_add@plt的第二條指令的地址(68 01 00 00 00 pushq $0x1);打印0x7FFFF7FC8046處地址存儲的值為:0x00000168 0x00,由此可得math_add@got.plt.item表項中存儲的值由0x1046變為0x0x7FFFF7FC8046。

所以將funcs.dso加載到進程的0x7FFFF7FC7000虛擬基地址處時,首先會更新.got.plt表中所有表項(不含前三項)存儲的值:*(math_add@got.plt.item) += 0x7FFFF7FC7000,這樣才能保證后面對math_add進行綁定時正確跳轉到math_add@plt的第二條指令,進而跳轉到.plt中調用dl_runtime_resolve函數執行真正的綁定操作。

看到沒有,實際的操作和之前講的還是稍微有點出入的。

驗證點3:主程序所依賴的直接或間接DSO對象是何時被加載映射到進程地址空間的。

上面講過,對DSO中全局變量的引用是無法PLT處理的,只要程序中有引用,不管執行的時候是否會被執行到,都要無條件的重定位;從這個例子可以推出:funcs.dso只對maths.dso中的方法有調用,所以在沒訪問maths.dso之前不應該加載maths.dso到進程地址空間的。

下面我們來驗證一下。

由上圖34,35的native process 58576可知,這個進程的ID=58576。

當前還沒有運行到math_add@plt過程,math_add.dso應該還沒加載。

通過cat /proc/58576/maps 指令可以查看進程的地址空間分布情況。

圖36

由圖36可知,此時已經將maths.dso加載映射到進程的地址空間了。

通過過以上三點可以得出結論:

1. 主程序直接或間接依賴的DSO對象,在主程序開始運行時會一次性加載映射到進程的地址空間。

2. 在主程序開始運行時,完成所有外部全局變量的重定位以及.rela.plt所有表項(不含前三項)的*(math_add@got.plt.item) += DSO_VIRT_BASE操作,上一步操作完后就知道每個DSO的虛擬基地址DSO_VIRT_BASE了。

3. 主程序開始運行時,其直接依賴的外部函數不是延遲綁定的,和對外部變量的處理一樣。

4. 共享對象中對外部函數的引用是使用PLT處理的,PLT機制有效。

在家窩了這些天,終于寫完了,也該告一段落了。

總結

以上是生活随笔為你收集整理的c++定义一个动态全局变量_静态链接与动态链接的宏观概述及微观详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

丁香色综合 | 日韩欧美在线观看 | 日韩精品首页 | 国产一区二区在线精品 | 99这里只有精品视频 | 亚洲国产中文字幕 | 久久电影色| 欧美色操| 亚洲国产三级在线 | 四虎影视久久久 | 中文字幕日本在线观看 | 激情综合六月 | 国产一二三四在线视频 | 欧美激情xxxx| 亚洲高清国产视频 | 9在线观看免费高清完整版在线观看明 | 毛片随便看 | 久久大视频 | 天天爱天天舔 | 中文字幕传媒 | 93久久精品日日躁夜夜躁欧美 | 国产在线美女 | 激情网综合 | 欧美精品亚洲精品 | 2023亚洲精品国偷拍自产在线 | 久久爱影视i | 日韩视频在线观看免费 | 99色资源| 欧美一二三视频 | 国产日韩精品在线观看 | 97福利在线观看 | 久久久天天操 | 一区二区三区在线观看中文字幕 | av片在线观看免费 | 国产a国产a国产a | 激情视频免费在线观看 | 国产精品九九九九九 | 国产丝袜一区二区三区 | 啪啪午夜免费 | 国产亚洲aⅴaaaaaa毛片 | 国产福利在线 | 久久激情五月丁香伊人 | 精品欧美一区二区精品久久 | 国产999视频在线观看 | 在线国产一区二区三区 | 欧美日韩精品在线播放 | 欧美在一区 | 天天综合在线观看 | 国精产品999国精产品视频 | 国产一二三四在线视频 | 玖玖视频网 | a久久久久久 | 中文字幕在线观看不卡 | 免费a视频在线观看 | 日韩一区二区三区免费视频 | 在线免费观看一区二区三区 | 中文字幕av网站 | 亚洲免费国产视频 | 激情偷乱人伦小说视频在线观看 | 亚洲一区动漫 | 深爱婷婷激情 | 精品久久综合 | 私人av| 精品一区 精品二区 | 久久久精品国产免费观看一区二区 | 综合久久五月天 | 91九色精品 | 4p变态网欧美系列 | 在线精品国产 | 亚洲免费av在线 | 麻豆免费看片 | 国产成人精品免高潮在线观看 | 欧美极度另类性三渗透 | 亚洲一区二区三区在线看 | 成人三级网站在线观看 | 久久精品久久精品久久39 | 婷婷中文字幕在线观看 | 久久99精品久久久久婷婷 | 日本公妇色中文字幕 | 中文字幕专区高清在线观看 | 伊香蕉大综综综合久久啪 | 久久99影院 | 国产精品视频在线看 | 中文字幕av一区二区三区四区 | 91 中文字幕 | 国产黄免费在线观看 | 97碰在线视频 | 视频在线观看亚洲 | 亚洲开心激情 | 色香天天 | 久久精品3 | 97在线观看视频免费 | 日韩欧美第二页 | 天天操月月操 | 天天操 夜夜操 | 人人爽人人搞 | 亚洲乱码精品 | 日一日操一操 | 深夜免费福利 | 精品字幕在线 | 亚洲欧美国产精品久久久久 | 亚洲天堂自拍视频 | 日韩欧美国产激情在线播放 | 久操操 | 免费性网站 | 国产精品麻豆欧美日韩ww | 国产精品嫩草55av | 国产乱视频 | 999久久| 亚洲欧美日韩一级 | av成人免费观看 | 国产在线91在线电影 | 久久免费在线观看视频 | 久草视频免费 | 日韩av一区二区在线影视 | 日本中文字幕在线 | 久久精品中文字幕 | 亚洲综合情 | 中文字幕专区高清在线观看 | 国产精品观看 | 国产日产精品一区二区三区四区的观看方式 | 综合久久综合久久 | 久久成人一区二区 | 国产美女免费观看 | 国产精品成人久久久久 | 91精品国产自产91精品 | 久久国产电影 | 99免费精品视频 | 亚洲码国产日韩欧美高潮在线播放 | 玖玖在线观看视频 | 久久免费中文视频 | 久久污视频 | www.888.av| 日韩精品不卡在线观看 | 午夜精品99久久免费 | 国产在线观看免 | 欧美久久精品 | 精品国产一区二区在线 | 96精品视频| 中文字幕视频免费观看 | 亚洲欧美日韩中文在线 | 亚洲人在线 | 又湿又紧又大又爽a视频国产 | 成人污视频在线观看 | 午夜精品视频福利 | 一区二区三区久久 | 啪啪av在线| 成人av免费播放 | 成人黄色电影视频 | av丝袜在线 | 久久一级电影 | 欧美aaa一级| 欧美一区二区日韩一区二区 | 美女国产精品 | 久久久久久看片 | 黄色毛片在线看 | 婷婷av色综合 | 欧美专区国产专区 | 中文字幕在线视频免费播放 | 永久免费av在线播放 | 亚洲视频网站在线观看 | av一区在线| 久久久精品99 | 青青河边草免费观看 | 成人av电影在线播放 | 99免费在线观看视频 | 色免费在线 | 日韩国产欧美在线视频 | 69视频在线播放 | 国产一级性生活视频 | 丁香婷婷社区 | 日韩av影视在线 | 天天综合网久久综合网 | 日日爽视频 | 在线免费观看视频一区 | 国产亚洲情侣一区二区无 | 91精品久久久久久综合五月天 | 九九精品无码 | 插插插色综合 | 青青河边草免费视频 | 97精品国产97久久久久久粉红 | 91资源在线观看 | 成人动漫精品一区二区 | 国内久久精品视频 | 亚洲视频大全 | 久久久免费| 天天射综合网视频 | 在线国产专区 | 久久99热这里只有精品国产 | 国产视频精品免费 | 国产精品a久久久久 | 国产人免费人成免费视频 | 在线亚洲小视频 | 国产精品久久久久久久久岛 | 亚洲精品免费在线视频 | www好男人 | 成人三级网站在线观看 | 99免费看片 | 中国精品一区二区 | 国产精品中文字幕在线 | 久久综合免费视频影院 | 黄色免费网站 | 毛片888| 天天天干夜夜夜操 | 99视频在线观看一区三区 | 国产精品99久久久久久久久 | 天天干,夜夜操 | 91在线精品秘密一区二区 | 免费一级日韩欧美性大片 | 香蕉视频国产在线观看 | 黄色成年 | 亚洲一二三久久 | 午夜视频一区二区三区 | 2023亚洲精品国偷拍自产在线 | 在线观看视频 | 91av资源在线 | 国产成人三级一区二区在线观看一 | 中文字幕91 | 久久国产精彩视频 | 狠狠操狠狠 | www国产亚洲精品久久麻豆 | 中文字幕永久在线 | 亚洲在线日韩 | 中文字幕在线观看一区二区三区 | 日韩精品不卡在线 | 日日夜夜天天 | 日韩v欧美v日本v亚洲v国产v | 99精品视频免费观看视频 | 最新中文字幕视频 | 国产精品av免费 | 国产精品自产拍在线观看中文 | 国产精品无av码在线观看 | 中文字幕中文 | 日韩免费观看一区二区三区 | 91精品日韩 | 超碰97公开 | 久久精品波多野结衣 | 丁香国产视频 | 国产精品一区二区吃奶在线观看 | 在线国产视频 | 国产精品a成v人在线播放 | 国产精品va视频 | 亚洲精品国| 日日射天天射 | 国产精品久久久777 成人手机在线视频 | 久草在线中文888 | 97视频资源| 激情五月激情综合网 | 国产精品 欧美 日韩 | 亚洲电影一区二区 | 国产高清成人 | 国产精品精品久久久 | 免费av在| 久久综合九色综合97婷婷女人 | 美女福利视频一区二区 | 日韩天天操 | 国产高清精品在线 | 97日日碰人人模人人澡分享吧 | 久久久精品一区二区三区 | 玖玖在线免费视频 | 美女免费视频一区二区 | 顶级欧美色妇4khd | 91精品国产成 | 国产拍揄自揄精品视频麻豆 | 一区二区三区在线不卡 | 亚洲男男gaygayxxxgv | 亚洲mv大片欧洲mv大片免费 | 黄色网在线播放 | 国产精成人品免费观看 | 亚洲人久久 | 免费麻豆视频 | 亚在线播放中文视频 | 五月婷婷综合在线观看 | av电影 一区二区 | 99精品久久只有精品 | 久久精品国产亚洲精品 | 日韩高清dvd | 五月黄色| 国产精品一区二区美女视频免费看 | 91麻豆视频网站 | 少妇高潮流白浆在线观看 | 久久99婷婷 | 久久久亚洲电影 | 五月婷婷导航 | 97超碰免费在线 | 午夜精品一区二区三区免费 | 亚洲.www| 国产免费嫩草影院 | 西西www444 | 日韩在线色视频 | 国产精品毛片一区二区三区 | www.色综合.com| 综合色久 | 欧美亚洲久久 | 日韩色在线观看 | 欧美日韩亚洲第一 | 亚洲综合涩 | 日韩黄色在线电影 | 99人久久精品视频最新地址 | 欧洲激情综合 | 日韩成人精品一区二区 | 国产69精品久久99不卡的观看体验 | 色天堂在线视频 | 一区二区三区不卡在线 | bayu135国产精品视频 | 免费看十八岁美女 | 亚洲国产成人精品久久 | 色综合久久久久久久 | 97超碰福利久久精品 | 国产亚洲精品成人av久久影院 | 国产精品淫 | 99久久精品国产欧美主题曲 | 天天综合网国产 | 最近中文字幕大全中文字幕免费 | 国产精品女教师 | 国产又粗又猛又黄又爽 | 国产偷国产偷亚洲清高 | 91精品一区在线观看 | 黄色特级一级片 | 国产精品视频永久免费播放 | 一区二三国产 | 日本精品久久久久中文字幕 | 99视频在线精品国自产拍免费观看 | 一区二区三区四区影院 | 天天干天天干天天干 | 亚洲精品久久久蜜臀下载官网 | 国产午夜精品一区二区三区在线观看 | 国产永久免费高清在线观看视频 | 91亚洲免费| 亚洲天堂香蕉 | 五月婷婷激情综合网 | 免费av在线网 | 亚洲不卡123 | 97在线视频免费 | 亚洲成人午夜在线 | 国产毛片在线 | 在线观看国产区 | 成年人免费在线观看网站 | 久久久久久网址 | 麻豆视频在线观看免费 | 日本色小说视频 | 在线之家官网 | 国产又黄又猛又粗 | 国内精品久久久久久中文字幕 | 国产剧情亚洲 | 久久久久国产一区二区三区 | 国产成人免费在线 | 日韩r级电影在线观看 | 四虎影视精品永久在线观看 | 久久久综合精品 | 久久精品成人欧美大片古装 | 三级小视频在线观看 | 欧美大码xxxx | 深夜激情影院 | 日日操日日干 | av爱干| 欧美人体xx | 最近中文字幕免费av | 偷拍精偷拍精品欧洲亚洲网站 | 日韩视频免费观看高清 | 丁香网婷婷 | 免费看的视频 | 欧美精品在线观看免费 | 亚洲三级影院 | 久久久国产一区 | 精品久久久久久久久久国产 | 草免费视频 | 91福利影院在线观看 | 午夜视频免费播放 | 干亚洲少妇 | 欧美日韩一区二区久久 | 手机av在线免费观看 | av黄色免费在线观看 | 日韩av三区 | a视频在线观看免费 | 亚洲伦理精品 | 在线免费看片 | 国产精品久久久久久久久久久久久 | 四虎最新域名 | 午夜国产在线观看 | 国产精品美女在线观看 | 色综合久久中文综合久久牛 | 欧美亚洲久久 | 欧美最新大片在线看 | 日本色小说视频 | va视频在线 | 在线播放精品一区二区三区 | 69国产精品成人在线播放 | 中文字幕av一区二区三区四区 | 三级黄色免费片 | 久久人人爽av | 日韩天天干 | 日本天天色 | 91欧美国产| 久久9精品 | 视频在线观看亚洲 | 欧美俄罗斯性视频 | 国产探花在线看 | 国产网站色 | 手机av在线网站 | 国产成人一区二区三区在线观看 | 91成人久久 | 久草久视频 | 黄色一区三区 | 欧美精品久久久久久久久久丰满 | 久久久久久久久免费视频 | 国产视频在线观看一区 | 色人久久 | 美女久久久 | 成人在线免费观看视视频 | 亚洲一区二区视频 | 日韩欧美视频在线观看免费 | 狠狠成人 | 久久精品视频免费 | 在线v| 久久久久国产精品一区二区 | 日本护士撒尿xxxx18 | 亚洲一区二区三区四区精品 | 在线免费观看黄网站 | 亚洲成人中文在线 | 欧美成人精品欧美一级乱黄 | www中文在线 | 91精品国产一区二区三区 | 中文字幕视频网站 | 国产一级精品视频 | 一级免费看视频 | 国产xxxxx在线观看 | 99热这里是精品 | 一本一本久久a久久精品综合 | 久草在线免费看视频 | 色婷婷欧美 | 国产高清精 | 日本女人在线观看 | 激情久久一区二区三区 | 国产精品观看在线亚洲人成网 | 欧美看片 | 六月婷婷久香在线视频 | 一区二区三区免费在线观看视频 | 日韩精品视频免费看 | 激情综合网五月婷婷 | 91在线亚洲| 五月天六月婷 | 亚洲黄网址 | 久久视频免费在线观看 | 亚洲一区二区黄色 | 免费在线视频一区二区 | 国产午夜小视频 | 亚洲精品日韩av | 国产黄色在线看 | 国产精品永久久久久久久www | 久久免费视频99 | 五月婷婷另类国产 | 一二三久久久 | 天天摸天天舔 | 婷婷激情5月天 | 久久久久久免费 | 狠狠色丁香婷婷综合久小说久 | 五月激情丁香图片 | 免费观看av| 日韩高清成人在线 | 欧美中文字幕久久 | 四虎在线免费观看视频 | 国产视频一区二区在线观看 | 亚州视频在线 | 久久精品日本啪啪涩涩 | 国产高清av在线播放 | 欧美日韩视频在线播放 | 久久涩视频 | 国产精品日韩在线观看 | 天天干天天操天天拍 | 91传媒在线 | 99人成在线观看视频 | 97在线免费| 五月婷婷在线视频观看 | 欧美成人性战久久 | 在线草| 操操操操网 | 欧美在线观看视频 | 久久99久久精品国产 | 色全色在线资源网 | 国产一区二区三精品久久久无广告 | 久草99| 中文字幕在线免费 | 久久久久国产精品免费 | 69久久久久久久 | 91精品国产91热久久久做人人 | 波多野结衣视频一区二区三区 | 国产96在线| 亚洲国产中文在线观看 | 久久久久久久久综合 | 少妇自拍av | 一区二区三区高清在线 | 成人av久久 | 亚洲成av人片在线观看香蕉 | 91麻豆文化传媒在线观看 | 96亚洲精品久久久蜜桃 | 99视频这里有精品 | 西西444www大胆高清视频 | 国产高清中文字幕 | 超碰公开在线观看 | 国产精品久久久久久麻豆一区 | 亚洲精品成人在线 | 久久视频| 国产美女被啪进深处喷白浆视频 | 久久久久久欧美二区电影网 | 国产精品电影在线 | 亚洲狠狠丁香婷婷综合久久久 | 国产精品久久久久婷婷 | 亚洲蜜桃av | 成人av网站在线观看 | 欧美成人va| 久久人人爽视频 | 91丨九色丨91啦蝌蚪老版 | 色.com| 欧美午夜a | 国产精品午夜在线 | 国产xx在线 | 精品亚洲视频在线观看 | 99精品在线免费观看 | 在线视频 一区二区 | 国产精品亚洲片夜色在线 | 久久国产福利 | 国产视频在线观看一区二区 | 丝袜美腿亚洲综合 | 在线免费视频你懂的 | 亚洲日本va午夜在线电影 | 国产无套精品久久久久久 | 天天拍天天干 | 成人三级av | 最近中文字幕大全 | av再线观看| 97中文字幕 | 国产精品久久电影网 | 欧美精品久久久久久久亚洲调教 | 99色免费视频 | 中文字幕 国产精品 | 成人黄色大片在线观看 | 不卡视频在线 | 国产精品久久久久久久久久久免费 | 日韩国产精品一区 | 视频国产一区二区三区 | 免费在线电影网址大全 | 国产第一二区 | 欧美性生交大片免网 | 久久综合免费视频 | 亚洲午夜久久久久久久久 | 成人性生活大片 | 91视频在线观看大全 | 成人动漫一区二区 | 亚洲在线视频观看 | 久草视频免费观 | 美女性爽视频国产免费app | 四虎在线免费观看视频 | 中文字幕高清 | www.日日日.com | 国产精品福利久久久 | 在线观看黄av | 日日夜夜骑 | 中文字幕日韩高清 | 日本久久中文 | 国产精品麻豆果冻传媒在线播放 | 亚洲精品国久久99热 | 亚洲精品影院在线观看 | 丁香五婷 | 国产一级a毛片视频爆浆 | 亚洲成a人片77777潘金莲 | 免费观看av | 天天拍天天爽 | 日韩电影在线看 | 四虎免费在线观看 | 91热这里只有精品 | 国产精品视频永久免费播放 | av综合站 | 超碰在线中文字幕 | 六月丁香激情综合 | 91精品久久久久久久久久入口 | 黄色成人影院 | 欧美天堂视频在线 | 婷婷六月综合网 | 日韩免费一二三区 | 日本三级在线观看中文字 | 欧美大片在线观看一区 | 亚洲成av人影院 | 日韩不卡高清视频 | 久久人人爽人人 | 欧美日韩国产一区二区三区在线观看 | 日本高清中文字幕有码在线 | 免费国产在线观看 | 免费在线观看一区二区三区 | 丁香六月婷婷综合 | 国产区高清在线 | 又黄又刺激视频 | 久久香蕉国产精品麻豆粉嫩av | 成人黄色在线看 | 免费a视频 | 精品国产免费看 | 日韩一区二区三区观看 | 国产亚洲在线视频 | 亚洲国产精品视频在线观看 | 日韩手机在线观看 | 国产精品99免视看9 国产精品毛片一区视频 | 91在线视频免费91 | 国产精品中文 | 国产亚洲精品久久久久久无几年桃 | 狂野欧美激情性xxxx | 在线 精品 国产 | 日韩精品视频在线观看网址 | 日日操天天爽 | 丁香花中文字幕 | 欧美另类xxxx| 精品国产一区在线观看 | 天天曰天天 | 97精品视频在线 | 男女男视频 | 91黄色小网站 | 在线观看精品一区 | 欧美日bb | 国产免费激情久久 | 久久久久一区二区三区四区 | 日韩高清无线码2023 | 月丁香婷婷| 久久综合九色综合欧美狠狠 | 91麻豆精品国产自产在线游戏 | 久久久电影 | 精品国产自在精品国产精野外直播 | www.夜夜操.com| 精品日韩视频 | 亚洲自拍av在线 | 国产在线播放不卡 | 国产一区国产二区在线观看 | 亚洲 在线 | 激情婷婷av| 久久av影院| 色综合久久中文字幕综合网 | 超碰av在线播放 | 免费观看日韩av | 久久激情视频 久久 | 九九视频一区 | 午夜精品久久久久久久久久 | 久久久久久久久影院 | 午夜精品av在线 | 免费在线观看成人小视频 | av日韩在线网站 | 精品免费| 国产伦精品一区二区三区… | 又黄又爽的免费高潮视频 | 天天操天天操天天操天天 | 国产在线观看,日本 | 久草在线免费看视频 | 91综合久久一区二区 | 91看国产 | 91精品久久久久久粉嫩 | 国产成人精品一区二区三区福利 | 国产在线无 | 一区二区电影在线观看 | 69精品视频在线观看 | 欧洲成人av | 国产精品私人影院 | 国产福利91精品一区二区三区 | 亚洲成色777777在线观看影院 | 免费99精品国产自在在线 | 丁香婷婷成人 | 国产1区2区3区在线 亚洲自拍偷拍色图 | 国产精品美女999 | 中文字幕二区三区 | 久久久久国产一区二区 | 国产99久久久欧美黑人 | 婷婷天天色 | 亚洲精品在线视频观看 | 91日韩在线播放 | 日韩色在线 | 中文字幕之中文字幕 | 97品白浆高清久久久久久 | 99精品视频中文字幕 | 91在线精品秘密一区二区 | 色综合五月 | 久久精品观看 | 91成人观看 | 国内精品毛片 | 91手机视频在线 | 久久久久国产一区二区 | 日本久久中文 | 日韩电影一区二区在线观看 | 91精品一区国产高清在线gif | 国产高清精品在线 | 国产视频 亚洲精品 | 免费观看www视频 | 五月香视频在线观看 | 久久成人免费电影 | 日韩在线观看中文 | 精品久久一 | 久久久国产一区二区三区 | 日韩欧美在线观看 | 久久亚洲专区 | 亚洲一二三在线 | 亚洲国产精品成人女人久久 | 91手机电视 | 婷婷综合成人 | 92精品国产成人观看免费 | 又黄又爽又刺激 | 亚洲免费一级 | 国内少妇自拍视频一区 | 欧美精品做受xxx性少妇 | 欧美性受极品xxxx喷水 | 欧美精品三级 | 色综合欧洲 | av7777777| 中文字幕麻豆 | av在线收看| 91成人精品一区在线播放69 | 91色影院| 不卡av在线 | 久久99精品波多结衣一区 | 成人久久久久久久久久 | 亚洲视频在线看 | 在线电影日韩 | 亚洲精品国产精品久久99 | 欧美夫妻性生活电影 | 久久免费精品一区二区三区 | 在线一区二区三区 | 五月天激情开心 | 日本久久久久 | 精品国产成人av在线免 | 免费看成人a | 日韩精品免费一线在线观看 | 人人干天天射 | 亚洲精品美女久久久 | 成年人视频在线免费播放 | 日韩理论片在线观看 | 成人av在线资源 | 国产中文字幕久久 | 欧美日韩国产在线精品 | 在线视频99 | 欧美精品免费一区二区 | 中文在线a天堂 | 999亚洲国产996395 | 日韩久久久久久久久久久久 | 国产精品美女久久 | 少妇bbbb搡bbbb搡bbbb | 99视频在线精品国自产拍免费观看 | 91精品婷婷国产综合久久蝌蚪 | 亚洲国产小视频在线观看 | 亚洲 欧洲 国产 精品 | 超碰99人人 | 免费网站污 | 久久精品一区二区三区视频 | www.少妇| 欧美亚洲xxx | 国产精品久久久久9999 | 亚洲综合爱 | 一级片免费观看 | 久久高清片 | 91久久国产综合精品女同国语 | 国产免费久久精品 | 亚洲春色奇米影视 | 国产精品2020| 草莓视频在线观看免费观看 | 亚洲国产片色 | 日韩视频a | 精品一区中文字幕 | 日韩av综合网站 | 久久久久成人精品免费播放动漫 | 国产精品免费观看视频 | 996久久国产精品线观看 | 久操视频在线免费看 | 成人黄色在线视频 | 人人干在线观看 | 99精品免费久久久久久久久 | 四虎在线免费观看 | 日韩高清免费在线观看 | 欧美成人一区二区 | 狠狠狠狠狠狠干 | 干天天| 国产高清av在线播放 | 99日精品 | 麻花豆传媒一二三产区 | 免费在线观看一级片 | 婷婷深爱五月 | 久久视频免费在线 | 国产视频在线观看一区 | 国产中文字幕久久 | 一区二区在线影院 | 激情九九 | 99精品视频免费观看 | 成人精品视频久久久久 | 亚洲午夜久久久久 | 国产精品精品久久久 | 日日色综合 | 97香蕉久久国产在线观看 | 天天综合天天做天天综合 | 亚洲自拍av在线 | 亚洲精品视频大全 | 丁香在线视频 | 国产精品欧美久久久久久 | 96久久欧美麻豆网站 | 国产精品九九视频 | 国产97色在线 | 精品国产视频在线 | 九九交易行官网 | 国产精品一区二区三区视频免费 | 欧美精品二区 | 成年人免费在线看 | 91九色综合 | av成人免费网站 | 国产区精品视频 | 中文字幕在线观看资源 | 国产黄色精品在线观看 | 国产淫片免费看 | 色国产精品一区在线观看 | 在线观看的av | 精品成人a区在线观看 | 在线视频 91 | 天天干天天弄 | 99在线热播 | 亚洲天天综合网 | 日韩特黄一级欧美毛片特黄 | 狠狠狠干 | 97香蕉超级碰碰久久免费软件 | 精品久久国产精品 | 亚洲黄色在线观看 | 久久97久久97精品免视看 | 四虎国产精品免费观看视频优播 | 粉嫩av一区二区三区四区五区 | 人人插人人澡 | 日韩精品不卡 | 日本不卡123 | 中文字幕在线免费播放 | 成人免费视频网 | 国产小视频你懂的 | 日本视频精品 | 一 级 黄 色 片免费看的 | 国产一区二区三区高清播放 | 免费在线观看av网址 | 中文字幕在线观看网站 | 69精品久久 | 91欧美视频网站 | 九九免费在线视频 | 九色视频自拍 | 中文字幕视频三区 | .精品久久久麻豆国产精品 亚洲va欧美 | 一区二区伦理 | 亚洲激情在线观看 | 婷婷国产在线 | 奇米先锋 | 久久综合偷偷噜噜噜色 | 国内精品中文字幕 | 黄色在线免费观看网站 | 欧美大片在线看免费观看 | 青青草久草在线 | 久久国产精品免费一区二区三区 | 天天操天天干天天操天天干 | 国产黄色观看 | 一区二区三区影院 | 91高清视频 | 九七视频在线 | 五月天综合色 | 99久热在线精品视频观看 | 国产中年夫妇高潮精品视频 | 日本中文字幕在线一区 | 超碰av在线 | 国产淫片免费看 | 国产视频在线看 | 久久影院一区 | 国产精品日韩 | 国产精品免费在线播放 | 国产69精品久久99不卡的观看体验 | 亚洲伊人成综合网 | 久久天天躁夜夜躁狠狠85麻豆 | 久久免费激情视频 | 国产欧美精品一区aⅴ影院 99视频国产精品免费观看 | 亚洲综合在线发布 | 亚洲国产高清在线观看视频 | 美女网站黄免费 | 美女网站在线看 | www.五月激情.com | www.天天干.com| 毛片网在线 | 亚洲一二三久久 | 亚洲涩涩涩 | 久久国产网站 | 中文在线字幕免 | 97精品国产97久久久久久久久久久久 | 99视频99| 97碰在线| 久久黄色影院 | 黄色三级久久 | 美女视频黄免费 | 亚洲精品资源在线 | 亚洲综合日韩在线 | 热九九精品 | 在线观看韩日电影免费 | 91色网址| 69精品视频在线观看 | 亚洲欧美激情精品一区二区 | 美女视频黄免费的久久 | 91桃色在线播放 | 亚洲黄色片| 日韩av电影中文字幕在线观看 | 国产精品久久久久久久99 | 在线观看中文字幕第一页 | 亚洲天天做 | 日韩视频一区二区三区 | 天天射天天舔天天干 | 久久久久久久久久久国产精品 | 国产精品美女久久久久aⅴ 干干夜夜 | 91亚州 | 欧美一级黄大片 | 久久av电影 | 一级黄色片在线观看 | 免费av在线播放 | 日韩二区在线观看 | 久综合网| 超级碰99 | 精品国产亚洲在线 | 免费看黄在线观看 | 91在线视频免费91 | 五月婷婷毛片 | 成年人av在线播放 | 国产精品高清免费在线观看 | 久久久久久久久久久免费视频 | 日韩免费在线观看视频 | 日韩精品一区二区三区免费视频观看 | 国产精品99久久久久久久久久久久 | 国产成人精品av | 亚洲专区一二三 | 中文字幕视频在线播放 | 香蕉一区 | 91资源在线播放 | 九九久久久久久久久激情 | 久久午夜电影院 | 国产九九九九九 | 女人18毛片90分钟 | 日韩色一区二区三区 | 国产a免费| 中文字幕亚洲不卡 | av免费播放| 亚洲综合色婷婷 | 97香蕉超级碰碰久久免费软件 | 中文字幕久久久精品 | 国产精品永久久久久久久久久 | 综合色在线 | 亚洲欧美成aⅴ人在线观看 四虎在线观看 | 在线a人片免费观看视频 | 精品国产免费看 | 国产午夜影院 | 日韩精品在线免费观看 | 国产成人一区二区三区免费看 | 91系列在线| 久久99精品国产99久久 | 免费男女羞羞的视频网站中文字幕 | 久久精品成人欧美大片古装 | 香蕉视频18 | 少妇精品久久久一区二区免费 | 在线观看91av | 久久视频一区二区 | 久久人人爽人人爽人人 | 亚洲第一区在线播放 | 亚州欧美视频 | 欧美日韩在线观看一区 | 久久成人高清视频 | 久久99免费观看 | 91av99| 久久国际影院 | v片在线看 | 精品国产一区二区三区免费 | 国产精品久久久久国产精品日日 | 国产麻豆精品一区二区 | 成人网看片| 日韩精品一区二 | 四虎8848免费高清在线观看 | 欧美色噜噜| 中文字幕精品一区二区三区电影 | 欧美在线free | 激情久久伊人 | av高清在线 | 久久午夜色播影院免费高清 | 人人插人人搞 | 91人人揉日日捏人人看 | 最近中文字幕免费观看 | 国产麻豆剧传媒免费观看 | 色天天久久 | 国产精品久久99综合免费观看尤物 | 99久久精品国产一区二区三区 | 欧美一级片在线 | 亚洲电影第一页av | 在线观看av网 | 最近中文字幕大全中文字幕免费 | 97色国产 | 久久精品视频在线观看 | 亚洲精品一区二区三区在线观看 | 日韩亚洲在线视频 | 美国人与动物xxxx | 色99视频 | 国产原创在线 | 91av在线精品 | 色婷婷99| www国产一区 | 91av官网|