1. 程式人生 > >茫無頭緒的瞎侃(一)

茫無頭緒的瞎侃(一)

前者稍微解除了一下PE檔案格式,這次簡要說一下PE的裝載,PE檔案中,所有段的起始地址都是頁的整數倍,段的長度如果不是

頁的整數倍,那就會對映時向上補齊到頁的整數倍,PE檔案中,聯結器在生產可執行檔案時,往往將所有的段儘可能的合併,所以一般

只有程式碼段,資料段,只讀資料段和BSS等為數不多的幾個段。

PE的術語中,有個相對虛擬地址的概念,其實噹噹與檔案中的偏移量。它是相對於PE檔案的裝載基地址的一個偏移地址。如果一個pe檔案

被裝載到虛擬地址0x00400000,那麼虛擬偏移地址為0x1000的地址就是0x00401000,每個pe檔案在裝載時都會有

一個裝載目標地址,即所謂的基地址。裝載一個PE可執行檔案的過程如下:

先讀取檔案的第一個頁,此頁包含了DOS頭,PE檔案頭和段表

檢查程序地址空間中,目標地址是否可用

使用段表中提供的資訊,將PE檔案中所有的段一一對映到地址空間中相應的位置

如果裝載地址不是目標地址,則進行Rebasing

裝載所有PE檔案所需要的DLL檔案

對PE檔案中的所有匯入符號進行解析

根據PE頭中指定的引數,簡歷初始化棧和堆

建立主執行緒並且啟動程序

PE檔案中,與裝載有關的資訊都包含在PE擴充套件頭和段表,具體結構如下,只分析32位

typedef struct _IMAGE_OPTIONAL_HEADER32{

WORD Magic;

BYTE MajorLinkerVersion,MinorLinkerVersion;

DWORD SizeOfCode;

DWORD SizeOfInitializedData;///初始化了的資料段長度

DWORD SizeofUninitializedData;///未初始化的資料段長度

DWORD AddressOfEntryPoint;////PE裝載器準備執行的PE檔案的第一個指令的RVA

DWORD BaseOfCode;////程式碼段起始RVA

DWORD BaseOfData;////資料段起始RVA

DWORD ImageBase;////PE檔案的優先裝載地址

DWORD SectionAlignment;////記憶體中段對齊的粒度,一般為4096

DWORD FileAlignment;////檔案中段對齊的粒度,一般為512位元組

WORD MajorOperatingSystemVersion;

WORD MinorOperatingSystemVersion;

WORD MajorImageVersion;

WORD MinorImageVersion;

WORD MajorSubsystemVersion;////程式執行所需要的子系統版本

WORD MinorSubsystemVersion;

DWORD Win32VersionValue;

DWORD SizeofImage;////記憶體中整個PE映像體的尺寸

DWORD SizeofHeaders;////所有頭+節表的大小,等於檔案尺寸減去檔案中所有節的尺寸

DWORD CheckSum;

WORD subsystem;////NT用來識別PE檔案屬於哪個子系統,GUI和CUI

DWORD sizeofStackReserve;

DWORD sizeofStackCommit;

DWORD sizeofHeapReserve;

DWORD sizeofHeapCommit;

DWORD LoaderFlags;

IMAGE_DATA_DIRECTORY DataDirectory[16];

}IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32

typedef struct _IMAGE_DATA_DIRECTORY{

DWORD VirtualAddress;

DWORD Size;

}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

關於動態連結

要解決空間浪費和更新困難的簡單辦法就是把程式的模組相互分割開來,形成獨立的檔案,而不再將他們靜態的連線在一起,

簡單來說,就是不對那些組成程式的目標檔案進行連線,等到程式要執行時才進行連線,也就是說,把連線過程推遲到執行時

在進行,這就是動態連線

動態連線的思想是把程式按照模組拆分成各個相對獨立的部分,在程式執行時才將他們連線在一起形成一個完整的程式,而不是

像靜態連結那樣把所有的程式模組都連線在一個單獨的可執行檔案。換句話說,動態連結把連線過程從本來的程式裝載前推遲到了

裝載的時候

在靜態連結時,整個程式最終只有一個可執行檔案,它是一個不可以分割的整體,但是在動態連線下,一個程式被分成了若干個檔案

有程式的主要部分,即可執行檔案和程式所依賴的共享物件,很多時候稱為模組。

/* Program1.c */

#include "lib.h"

int main()

{

show(1);

return 0;

}

/* Program2.c */

#include "lib.h"

int main()

{

show(2);

return 0;

}

/* lib.c */

#include <stdio.h>

void show(int i)

{

printf("Printing from lib.so %d\n",i);

}

/**  lib.h **/

#ifndef LIB_H

#define LIB_H

void show(int i);

#endif

下面是程序執行時的虛擬地址空間分佈

$cat /proc/12985/maps

08048000-08049000 r-xp0000000008:011343422./Program1

08049000-0804a000rwxp0000000008:011343432./Pragram1

b7e83000-b7e84000rwxpb7e8300000:000

b7e84000-b7fc8000r-xp0000000008:011488993/lib/tls/i686/cmov/libc-2.6.1.so

b7fc80000-b7fc9000r-xp0014300008:011488993 /lib/tls/i686/cmov/libc-2.6.1.so

b7fc9000-b7fce000r-xp0014400008:011488993 /lib/tls/i686/cmov/libc-2.6.1.so 

b7fcb000-b7fce000rwxpb7fcb00000:000

b7fd8000-b7fd9000rwxpb7fd800000:000

b7fd9000-b7fda000r-xp0000000008:011343290./lib.so

b7fda000-b7fdb000rwxp0000000008:011343290./lib.so

b7fdb000-b7fdd000rwxpb7fdb00000:000

b7fdd000-b7ff7000r-xp0000000008:011455332/lib/ld-2.6.1.so

b7ff7000-b7ff9000rwxp0001900008:011455332/lib/ld-2.6.1.so

bf965000-bf97b000we-pbf96500000:000[stack]

ffffe000-fffff000r-xp0000000000:000[vdso]

可以看到,整個程序虛擬地址空間中,多出了幾個檔案的對映。lib.so與program1一樣,都是被作業系統用同樣的方法對映到程序的虛擬地址空間。

關於ld-2.6.so,實際刪格式linux下的動態聯結器,動態聯結器與普通共享物件一樣被對映到了程序的地址空間,在系統開始執行program1之前,

首先會把控制權交給動態聯結器,由他完成所有的動態連結工作以後再把控制權交給program1,然後開始執行。

共享物件的最終連結裝在地址在編譯時是不確定的,而是在裝載時,裝載器根據當前地址空間的空閒情況,動態分配一塊大小的虛擬地址空間給

相應的共享物件。

其實程式模組的指令和資料中可能會包含一些絕對地址的引用,在連線產生輸出檔案的時候,就要假設模組被裝載 的目標地址。但是共享物件在

編譯時不能假設自己在程序虛擬地址空間中的位置。與此不同的是,可執行檔案基本可以確定在程序虛擬空間中的起始位置,因為可執行檔案往往

是第一個被載入的檔案,它可以選擇一個固定空閒的地址。在連線時的重定位稱為連線時重定位,而此時,裝載時也需要對模組地址進行重定位

,我們稱為裝載時重定位。而windows下又稱為基址重置。

裝載時重定位雖然解決了動態模組中絕對地址引用,但是使得指令部分無法再多個程序之間共享,此時我們希望程式模組中共享的指令部分在

裝載時不需要根據裝載地址的改變而改變,所以需要把指令中那些需要被修改的部分分離出來,和資料部分放在一起,這樣指令部分就可以保持

不變,而資料部分可以在每個程序中擁有一個副本,這種方法被稱為地址無關程式碼PIC。

其實產生地址無關程式碼並不麻煩,先將模組中各種型別的地址引用方式是否跨模組分為兩類:模組內部引用和模組外部引用:按照不同的引用

方式又可以分為指令引用和資料訪問。此時分為四種情況

模組內部的函式呼叫、跳轉等

模組內部的資料訪問,比如模組中定義的全域性變數、靜態變數

模組外部的函式呼叫、跳轉等。

模組外部的資料訪問,比如其它模組中定義的全域性變數。

static int a;

extern int b;

extern void ext();

void bar()

{

a = 1;                       ///模組內部資料訪問

b = 2; ///模組外部資料訪問

}

void foo()

{

bar(); ///模組內部函式訪問

ext(); ///模組外部函式訪問

}

當編譯器在編譯此檔案時,實際上不能確定變數b和函式ext() 是模組外部的還是模組內部的,因為它們有可能定義在同一共享物件的其它目標檔案

中。由於沒法確定,編譯器只能把它們都當做外部函式和變數來處理。msvc編譯器提供了__declspec(dllimport)擴充套件來標識一個符號是模組內部

還是模組外部的。

第一種情況中,對於被呼叫函式和呼叫者都處於同一個模組,他們之間的相對位置是固定的,因此模組內部的跳轉、函式呼叫都可以是相對地址

呼叫,或者基於暫存器的相對呼叫,所以對於這種指令是不需要重定位的。

<bar>:

8048344:55push %ebp

8048345:89  e5mov %esp,%ebp

8048347:5dpop %ebp

8048348:c3ret

8048349:<foo>:

......

8048357:e8  e8  ff  ff  ffcall 8048344 <bar>

804835c:b8  00  00  00 00mov $0x0,%eax

......

foo中對bar的呼叫那條指令實際上就是一條相對地址呼叫指令,此條指令中後4個位元組是目的地址相對於當前指令的下一條指令的偏移,即

0xffffffe8,0xffffffe8是-24的補碼形式,即bar的地址為0x804835c-24 = 0x8048344 只要bar和foo的相對位置不變,這條指令是地址無關的,

這種相對地址的方式對於jmp指令也是有效的。

很明顯,指令中不能直接包含資料的絕對地址,唯一的辦法就是使用相對地址,一個模組前面一般是若干個頁的程式碼,後面緊跟若干個頁的資料

這些頁之間的相對位置是固定的,如此,任何一條指令與它需要訪問的模組內部資料之間的相對位置是固定的,只需要相對於當前指令加上固定

偏移量就可以訪問模組內部的資料了。

00000044c<bar>:

44c:55 push %ebp

44d:89 e5 mov %esp,%ebp

44f:e8  40  00  00 00   call 494 <__i686.get_pc_thunk.cx>

454:81  c1  8c  11  00  00add  $0x118c,%ecx

45a:c7  81   28  00  00  00  01movl $0x1,0x28(%ecx)

461:00  00  00

464:8b  81  fb  ff  ff  ffmov 0xfffffff8(%ecx),%eax

46a:c7  00  02  00 00 00movl $0x2,(%eax)

470:5d pop %ebp

471:c3 ret

00000494 <__i686.get_pc_thunk.cx>

494:8b  0c  24mov (%esp),%ecx

497:c3 ret

當處理器執行call指令以後,下一條指令的地址就會被壓到棧頂,而esp暫存器始終指向棧頂,當"__i686.get_pc_thunk.cx"執行"mov (%esp),%ecx"

時,返回地址就被賦值到ecx暫存器了。

接著執行一條add和一條mov,就可以看到遍歷a地址是add指令地址(儲存在ecx暫存器)加上另個偏移量0x118c和0x28,即如果模組被裝載到

0x10000000這個地址,那麼變數a的實際地址是0x100000000 + 0x454 +0x118c + 0x28 = 0x10001608 如圖

|--------------------------------------------------------------------------------0x00000000 

|

|

|

|

|---------------------------------------------------------------------------------0x10000000

|--------- |  44f:e8  40  00  00 00   call 494 <__i686.get_pc_thunk.cx>

|| 454:81  c1  8c  11  00  00add  $0x118c,%ecx

||  45a:c7  81   28  00  00  00  01movl $0x1,0x28(%ecx)

||  461:00  00  00

||.text

0x118c  +  0x28   |

||

||--------------------------------------------------------------------------------------

||

|--------- |static int a;

|

|

|.data

|

|----------------------------------------------------------------------------------------

而模組間的資料訪問,需要等到裝載時才決定,例如變數b,被定義在其它模組中,並且改地址在裝載時才能確定,使得程式碼地址無關,基本

思想就是把跟地址相關的部分放到資料段中,很明顯,這些其它模組的全域性變數的地址是跟模組裝載地址有關的,此時在資料段建立一個

指向這些變數的指標陣列,也稱為全域性偏移表GOT,當代碼需要引用該全域性變數時,可以通過GOT中相對應的項間接引用。當指令中需要訪問

變數b時,程式先找到GOT,此時根據GOT中變數所對應的項找到變數的木匾地址,每個變數對應一個4位元組的地址,聯結器在裝載模組的時候

會查詢每個變數所在的地址,填充GOT中各個項,以確保每個指標指向地址正確。由於GOT本身是放在資料段的,所以它可以在模組裝載時被修改

,並且每個程序都可以有獨立的副本。

模組在編譯時可以確定模組內部變數相對與當前指令的偏移,那麼我們也可以在編譯時確定GOT相對於當前指令的偏移即確定GOT的位置,然後根據

變數地址在GOT中的偏移就可以得到變數地址。

但是定義在模組內部的全域性變數該如何處理呢?比如一個共享物件定義了一個全域性變數global,在模組module.c中是這麼引用的

extern int global;

int foo()

{

global = 1;

}

此時編譯器編譯module.c時,無法根據上下文判斷global是定義在同一個模組的其它目標檔案還是定義在另外一個共享物件之中,即無法判斷

是否跨模組呼叫,也就是無法判斷是通過GOT方式引用還是在本地可執行檔案.bss中。此時我們把所有的使用這個變數的指令都指向位於可執行檔案

中的那個副本。elf共享庫在編譯時,預設把定義在模組內部的全域性變數當做定義在其他模組的全域性變數,通過GOT來實現變數的訪問。當共享模組

被裝載時,如果某個全域性變數在可執行檔案中擁有副本,那麼動態連結器就會把GOT中相應地址指向該副本,這樣該變數在執行時實際上最終只有

一個例項。如果變數在共享模組中被初始化,俺麼動態聯結器還需要將該初始化值複製到程式主模組中的變數副本。如果該全域性變數在程式主模組中

沒有副本,那麼GOT的相應地址就指向模組內部的該變數副本。

如果lib.so中定義了一個全域性變數G,而程序A和程序B都是用了lib.so,那麼當程序A改變G時,程序B會受到影響嗎?

不會,當lib.so被兩個程序載入時,它的資料段部分在每個程序中都有獨立的副本,此時,共享物件中的全域性變數實際上和定義在程式內部的全域性變數

沒什麼區別,任何一個程序訪問的只是那個副本,而不會影響到其它程序,但是如果是同一個程序的執行緒A和執行緒B,此時是會影響到的。此時windows

上有個專門的術語執行緒私有儲存(Thread Local Storage).

此時我們該對比一下靜態連結和動態連線的區別了,動態連線比靜態連結慢的主要原因是動態連結下對於全域性和靜態的資料訪問都要進行負載的GOT

定位,並進行間接定址;對於模組間的呼叫也要先進性GOT,然後間接跳轉。如此一來,必然使程式的執行速度收到影響。同時,動態連結的連線工作

在執行時完成,即程式執行時,還要進行一次連線工作,裝載所需要的共享物件,然後進行符號查詢地址重定位等工作。針對第二種情況,基於共享物件

中的很多函式不會被用到,如果一開始就把所有函式都連線實際上也是一種浪費。所以此時採用一種延遲繫結的做法,基本思想就是,函式第一次

用到時才進行繫結(符號查詢、定位)

elf使用PLT(Procedure Linkage Table)方法實現。假設liba.so需要呼叫libc.so中的bar函式,那麼當liba.so第一次呼叫bar函式時,需要動態聯結器

中的某個函式來完成地址繫結工作,我們假設lookup()來查詢bar地址,此時lookup需要知道地址繫結發生在哪個模組,哪個函式。

lookup(module,function),當呼叫外部模組的函式時,通常用GOT中相應項進行間接跳轉,PLT為了實現延遲繫結,又增加了一層間接跳轉。

此時,每個外部函式在PLT中都有一個相應的項,比如bar的項地址為[email protected]。實現如下

[email protected]:

jmp *([email protected])

push n

push moduleID

jump _dl_runtime_resolve

很明顯,第一條指令的效果就是跳轉到第二條指令,而第二條指令將n壓入堆疊,這個數字是bar這個符號引用在重定位表的.rel.plt的下標,接著

又是將moduleid壓入堆疊,然後跳轉到_dl_module_resolve

這實際就是lookup(module,function)的呼叫。

其實PLT真正實現起來要複雜一些,elf將GOT拆分為.got和.got.plt,其中.got用來儲存全域性變數引用地址,而.got.plt用來儲存函式的地址,對於外部函式

的引用部分被分離出來放入.got.plt中,另外.got.plt的前三項如下:

第一項儲存的是.dynamic段的地址,此段描述了本模組動態連線相關的資訊

第二項儲存本模組的ID,第三項儲存的是_dl_runtime_resolve的地址。

相關推薦

茫無頭緒()

前者稍微解除了一下PE檔案格式,這次簡要說一下PE的裝載,PE檔案中,所有段的起始地址都是頁的整數倍,段的長度如果不是 頁的整數倍,那就會對映時向上補齊到頁的整數倍,PE檔案中,聯結器在生產可執行檔案時,往往將所有的段儘可能的合併,所以一般 只有程式碼段,資料段,只讀資料段

編譯原理的“文法”

das 目的 組成 spa 希望 英語 什麽 條件 sin 如果你翹累了代碼,想喝喝咖啡,順便看點兒可以當佐料的文章那本文應該比較適合現在的你。(•??•?)? ?? 我們一天天都在和代碼打交道,但是你了解代碼的運行原理麽?為什麽你的一行代碼就能被執行

隨筆(很晚很晚,通)

         凌晨,自己人沒有入睡,剛剛完成學生科負責人得面 試,我準備留下來,我不知道是對是錯,但我已經做出了選擇,既然選擇了,那就無怨無悔。把自己的時間安排好,不能因為這一件事情浪費太多的時間。      

人工智慧2-零散的人工智慧概念

宣告:本人並未深入研究人工智慧技術,也暫無此打算,主要談自己的理解,以下內容僅供參考。 人工智慧概述 人工智慧現狀如下圖 從大到小的概念依次是:人工智慧(AI)->機器學習->表徵學習->深度學習     機器學習主要靠的是邏輯迴歸,表徵學

人工智慧1-智慧是為了預測

宣告:本人並未深入研究人工智慧技術,也暫無此打算,主要談自己的理解,以下內容僅供參考。 什麼叫做智慧?     在我看來,能夠基於過去的事實標定出未來事物發展範圍的東西就可以被稱為智慧。人也是這樣,基於過去的記憶進行思維來決定下一刻要幹什麼。因此智慧的最終目的是“預測”。

看完就能出去神,來自研發第一線的“區塊鏈”掃盲文(

區塊鏈 代幣 Blockchain 麻將 這兩年要說什麽概念最火,那一定是區塊鏈了。現在要是自己不說一說什麽是公鏈、私鏈,什麽是Token,都不好意思說自己是做IT的。可是什麽是區塊鏈?它的出現能解決我們現實生活中什麽問題?什麽是代幣?什麽是共識機制?什麽是智能合約?這

折騰之個人學習環境搭建():安裝XenServer

背景介紹 家裡只有一臺安裝WIN10的筆記本可供在下折騰,在下先安裝了個VMware® Workstation 12 Pro,計劃建立一臺虛擬機器安裝XenServer,再得用XenServer虛擬3臺Centos7,夠折騰的吧,其實如果僅虛擬Centos7,可以直接使用VMwar

段阿里筆試程式碼的()分析

本文地址 前言 我是菜雞,如有不對的地方煩請指正。 起始 上週(好像是上週)的時候作業系統的老師丟擲了一個問題留給我們: 對以下程式在一臺主流配置 的PC上,呼叫f(36)所需要 的時間大概是多少?請給出時間估算的依據並對程式的執行情況進行詳細的解析說明

掰大資料-- 世界的本質是資料,胡掰,專注微軟大資料解決方案

1. 技術負債在敏捷團隊中會快速的膨脹。 2. 敏捷軟體開發團隊會想當然地認為每個團隊成員都專業,稱職並富有責任心。如果事實不是如此,專案開發很快會變得舉步維艱。 3. 由於對敏捷開發實踐的錯誤理解,導致團隊不合理地頻繁交付,疲於奔命。 4. 實施敏捷的門檻太高,敏捷開發需

什麼,這些人你還不認識?!文帶你有姿勢地深度學習大佬

大資料文摘作品編譯:餘志文,笪潔瓊,錢天培近幾年間,深度學習的興起造就了一批超級巨星。一向在學術

Python 接口測試(

blog 系統環境變量 resp 環境變量 nbsp 們的 www nload uic 1. 概念: 接口測試是測試系統組件間接口的一種測試。接口測試主要用於檢測外部系統與系統之間以及內部各個子系統之間的交互點。測試的重點是要檢查數據的交換,傳遞和控制管理過程,以及系統間的

【內存優化】加載張圖像資源到底占據多少內存

div blog 效果 .get round raw tails 整體 spa 0.內容概覽 1. 簡介 2. 問題 3. 概念描述 4. 具體分析 5. 總結 6. 參考文檔 1.簡介 Android中經常要通過ImageView進

《代碼閱讀》讀書筆記(

需求 的人 一行 編碼 重要 流動 使用 分析 缺少 《代碼閱讀》讀書筆記(一) 《代碼閱讀》(《Code Reading The Open Source Perspective》)Diomidis Spinellis 著 ---------------------

豎四象限 工作法

坐標系 平面 十字路口 象限工作法 工作用遇到很多問題,每個人都說自己的需求著急,都要立刻解決,很是頭痛,可能很早就有象限工作法,但我才剛剛看到,覺得有用,在這裏簡單說明下: 首先我就不明白什麽叫"象限",度娘告訴我,它就是平面直接坐標系中每一個區域叫做一個象限,還是不明白?可以

智能提示() Solr (suggest)

watermark 搜索 gen ets con 技術 結果集 推薦 ack 電商搜索中要實現這麽一塊功能,當輸入文字時候。下拉框提示。類似於百度搜索 在師出名門的基於lucene的solr搜索引擎中。提供了 拼寫檢查和智能提示這塊功能。 拼寫檢查就是用來檢

jsp中讀取數據庫內容(

reat cor mysq ava roo 數據 state imp nav 在jsp中將數據庫表格內容讀出為一個表格,並在表格中添加超鏈接: 1 <%@ page language="java" contentType="text/html; charset=U

種大氣簡單的Web管理(陳列)版面設計

borde absolut setup hid color 正常的 for pre == 在頁面的設計中,多版面是一種常見的設計樣式。本文命名一種 這種樣式。能夠簡單描寫敘述為一行top,一列左文件夾,剩余的右下的空間為內容展示區。這種樣式,便於高速定位

數據驅動安全架構升級---“花瓶”模型迎來V5.0()

安全模型 雲計算 花瓶 大數據 保障方案 數據驅動安全架構升級---“花瓶”模型迎來V5.0 Jackzhai 一、背景近十年,可以說是網絡技術大發展的十年,雲計算、大數據、移動互聯、物聯網等新技術逐漸成熟,社交、電商、智慧城市…現實社會正在全面走進網絡所構建的虛擬世界,網絡正在成為人們吃

次mapreduce讀取不到輸入文件的問題

mapreduce 過濾器hdfs上輸入文件所在包含兩個目錄,分別是: /20170503/shoplast/ /20170503/shop/但是我想過濾掉shop,只把shoplast作為輸入故我實現了過濾器如下: public static class Fi

AngularJS入門學習筆記

rect directive 技術分享 attr 兩個 ava 內容 module 大括號 首先聲明: 本博客源自於學習:跟我學AngularJs:AngularJs入門及第一個實例。通過學習,我自己的一些學習筆記。 1.AngularJS的一些基本特性 (1)使用雙大括號