c言語程序經過編譯後,每條指令都有一個內存地址,那兩個程序假如有相同內存地址的指令怎樣辦?
這要分兩大類三種狀況評論。
榜首大類是實形式下跑的各種運用,此刻代碼中的地址便是物理地址。
這個大類又分兩種狀況。
一是單片機那樣,地址便是終究的物理地址,將來就要分毫不差的寫進內存晶片對應的單元裡。這種狀況下天然不或許完結一起載入兩個地址牴觸的程序這樣的使命。
二是DOS那樣,程序中的地址並不是終究的物理地址,而是所謂的「相對地址」;OS載入程序時必須先做一個「重定位」,比方前200k的內存被他人佔用了;那麼當載入你的程序時,就從201K開端(一起把201K這個值載入基址寄存器)。當遇到跳轉指令時,只需把裡邊寫死的相對地址提取出來、加上基址寄存器的值(也便是201K)即可。這便是運轉時的重定位(對開端編譯出的.o/.obj文件來說,在連結時也需求做一次重定位,原理相似,也是給裡邊的全部涉及到的地址加上一個固定的偏移;但這次重定位是靜態的,和載入履行時的動態重定位不相同)。
實踐上,現在的內核代碼仍然需求跑在實形式下,因而運轉時的動態重定位仍然是必需的;別的相對定址也有很多種不同辦法,這兒僅僅是個原理性的簡化說法。
第二大類是虛地址形式下跑的各種運用,也便是第三種狀況。
這個形式下,OS一般會事前做一個約好,比方內存前1G(或更大空間)歸於OS,其它空間歸於運用。實在運轉時,經過虛擬地址映射機制,OS內容就被映射到了約好好的空間、而用戶運用則從約好好的開端點開端次序載入(實踐中也或許故意在每次載入時都偏移一個隨機值,這樣萬一有緩衝區溢出之類縫隙,黑客們想完結進犯就沒那麼簡略了。究竟重定位是個早已被處理的問題,不用白不用)。
憑藉虛擬地址機制,每個運用都會「覺得」自己獨佔了悉數內存空間,並不能意識到其它運用的存在。
所謂虛擬地址機制,說白了和實形式下的「基址相對定址」是一個原理;僅僅現在擔任內存映射的寄存器對運用來說不再可見了罷了——實形式下,運用知道自己被載入到了201K開端處,因而當它要拜訪內存地址1235時,實踐拜訪的是201K+1235處,全部全部它都知道的清清楚楚;而虛形式下,運用以為自己便是從0開端載入的,拜訪1235便是1235,並不知道實踐上OS「悄悄」替它加了個偏移值、使它拜訪到了正確的當地(這個偏移值被OS主動保護在「頁表」裡,每個進程都有獨歸於自己的一套獨自的頁表;當這仍然是個簡化的、原理性的說法,實踐上還有頁式辦理、段頁式辦理等不同分類,並且不同的CPU支撐的辦理計劃也有所不同)。
有了虛擬地址機制,當體系資源缺乏時,某些運用的某些內存頁面就或許被暫存於磁碟(對windows,默許是c:pagefile.sys;當然你能夠經過裝備改動方位),然後儘量騰出內存條空間給前臺運用——當你在內存小於8G(乃至4G)的電腦上一起開N個巨耗內存的運用時,就會發現機器運轉緩慢、硬碟燈不斷的閃耀(一起伴跟著咯吱咯吱的讀盤聲,假定你用的仍是機械硬碟的話)。原因便是內存缺乏,OS不得不在內存和硬碟間不斷倒騰,獻身速度儘量確保使命完結。這個進程中,程序被換出/換入內存的那部分頁面就必須更新它在頁表中的索引信息,這才幹隨意搬哪都不阻礙程序的運轉。
憑藉相似辦法,一個運轉中的運用乃至能夠暫時被儲存到磁碟上、然後斷電關機;等下次計算機重啟後再從磁碟載入這個運用對應的內存數據(到隨意什麼當地)、讓這個運用從前次關機的當地持續運轉(比方你玩槍戰遊戲時,能夠在槍榴彈出膛時休眠斷電、等半個月後從頭加電開機,你會看見畫面上那顆槍榴彈持續飛向方針……)——當然,儘管原理相同,但這時分用的或許並不是交流文件(比方,假如你沒有改正裝備的話,Windows下便是C盤根目錄下的hiberfil.sys)。
兩位高贊答主說的十分全面了,我寫一段省流量簡化版。
題主現已發現了對立點,呈現對立的原因是:「
c言語程序經過編譯後,每條指令都有一個內存地址
」,這句問題描繪是過錯的。
編譯的時機只要一次,假如在編譯時直接指定代碼的內存地址,也便是「指定代碼放在內存的哪裡」,顯然是不或許的。
就像去校園浴室洗澡。每次去洗澡,浴室大叔會挑一個空衣櫃給你鑰匙,即動態給你分配一個編號。這個編號每次去都會變。
相同的原理能否用在程序載入時呢?最簡略的辦法便是——編譯器僅開端指定代碼的內存方位,假定代碼從0開端,那麼榜首句代碼在地址1,第2句代碼在地址2,榜首個跳轉去100。這樣做,把相對方位先定下來。
實踐載入程序運轉的時分,操作體系會挑一塊能放下程序的閒暇空間給你,比方給了你22222開端地址。那麼前面的1、2、100就順勢改為22223、22224、22322,即可。
以上說的便是樸實的原理了,不管在什麼體系上,其實質原理不會變。
至於實用化的操作體系,會將上述進程封裝再封裝,形成像「虛擬地址」這樣的高檔概念,讓你誤以為每次棧頂地址都是相同的。
虛擬地址就相當於:老大爺每次給你的柜子編號都是1,讓你覺得如同受到了優待。但實踐上每次1對應的都是不同的柜子。究竟哪個編號對應哪個柜子,只要老大爺知道。
不管操作體系怎樣雜亂,算法的實質不會變,也無法改動。
更有意思的是,當C/C++載入動態連結庫時,狀況會變得愈加錯綜複雜。假如對載入的原理感興趣,引薦《程式設計師的自我涵養》一書。
很意外如同沒有人答對(在寫本答覆的時分如同只要一個人答覆沾邊),大部分答覆都在談內存地址的虛擬化,那麼在CPU有保護形式(內存地址的虛擬化)之前,莫非就只能一起載入一個程序?
(修正:說虛擬內存的錯在邏輯上舍本求末。就如同吊葡萄糖能夠彌補能量,吊的時分不吃飯也行,可是說是吊葡萄糖處理了吃飯問題這便是邏輯上的舍本求末。)
歡迎來到凱叔講故事。
其實這個問題的要害和處理的辦法並不是內存虛擬地址,儘管內存虛擬地址的確形似能夠處理這個問題(其實並不能,最終有說到),可是前史上這個問題的呈現和處理遠早於保護形式(內存虛擬地址)的呈現。
這個問題的隱秘在於EXE/ELF等可履行文件格局上。這些可履行文件格局的創造和擬定,便是為了處理這個問題。
在好久好久曾經(其實也便是30多年前),電腦程式的確是只能載入到內存傍邊的固定方位的。比方,BOOTLOADER、以及DOS發動時所需的那3個聞名文件(這三個模塊是輸入輸出模塊(IO.SYS)、文件辦理模塊(MSDOS.SYS)及指令解說模塊)便是這樣。
然後,就有了題主所憂慮的問題,很不便利。
所以,就有了. COM的格局(或許還不是最早的,僅僅比較聞名的)。. COM格局答應將程序放在恣意一個內存分段傍邊,可是程序在段傍邊的偏移地址是固定的,也不能有別的獨立的數據段。整個程序的巨細(含數據)不能超越64K,由於這是一個內存段的巨細。
內存需求分段,以及一個段是64K的原因是,那個時分的CPU本身是16bit的(e,疏忽更老的8bit CPU不談),便是全部寄存器都是16bit的,可是地址總線是20bit的。這樣的話,就需求將一個被稱為段寄存器的內容左移4bit然後加上別的一個寄存器(稱為偏移寄存器)的內容,才幹得到20bit的內存地址。16bit正好64K,也便是在不修正段寄存器的狀況下,能夠直接定址的規模便是64K。超越這個規模就需求修正段寄存器。這在曾經也叫做長跳轉。
可是後來跟著程序越來越大,64K現已放不下了。首要想到的是將程序傍邊的數據部分別離出去放在獨自的段傍邊,這便是數據段。然後程序本身也或許需求跨多個段。
可是這樣仍是不是很便利。由於數據段別離出去了,有的程序段或許很小。可是假如全部程序都是從一個固定的偏移地址開端,那麼這些程序至少不能放在同一個段內。這就比較糟蹋內存,特別是在沒有保護形式(虛擬地址)的時代,每個段可便是實打實的64K物理內存。那個時分,總共物理內存大部分也就256K,1M現已算高端機了(1M是20bit地址總線的定址上限),所以並沒有那麼多段可用。
所以就有了起浮二進位可履行方法,這便是EXE或許ELF等。這種可履行文件傍邊的地址僅僅關於程序傍邊某個方位(比方main函數開端方位)的一個偏移量,操作體系能夠將其載入到內存任何方位,然後依據實踐main函數進口所在地址和這些偏移量,去更新機器碼傍邊全部的地址參照,將其改為實踐地址。也便是,在程序實踐被履行之前,操作體系會對其進行一次修正/更新。
而題主用objdump所看到的「地址」,其實便是這個相關於程序本身的偏移地址,並不是內存地址,即使是保護形式下,也不是依照這個地址履行的。當然,愈加精確地來說,還要看被objdump的是什麼文件。假如是. obj/.a文件,也便是連結之前的文件,那麼這個偏移僅僅關於. obj/.a本身進口的偏移,在C言語連結的這一步,linker會將這些. obj/.a安排到可履行文件傍邊,並且更新這些偏移。(然後在可履行文件被載入到內存之後,操作體系還會再次更新。是的,這個進程和linker很像)
後來,跟著32bit/64bit CPU的呈現,CPU定址規模大大添加,才呈現所謂的flat形式內存空間,也便是不分段(整個內存空間只要一段)。可是即使這樣,在運用動態庫等的時分,仍然存在動態庫載入到哪裡的問題,由於不同的宿主程序長度不同,動態庫載入數量和次序不同,即使有虛擬地址空間,同一個動態庫在不同程序的虛擬地址空間傍邊也不或許永久都載入到同一個方位。
此外還有安全方面的考慮。假如程序每次都載入到相同的當地,那麼程序傍邊的每個變數也很或許每次都呈現在相同的方位,尤其是大局變數。這使得程序十分簡略被監督及hack。所以每次讓操作體系將其載入到不同方位,能夠進步一點兒安全性。
總歸,這個問題其實與虛擬地址空間並沒有什麼關係。儘管虛擬地址空間的確能夠防止程序之間的內存拜訪牴觸,可是前史上並不是用虛擬地址空間處理的這個問題,並且它處理不了動態庫的載入問題。
你說的是裸機,的確會有這種狀況。
可是進入操作體系之後,你的程序看到的內存滿是假的,乃至有或許對應的地址底子就不在內存,而是在硬碟上。
由於操作體系會為每個進程虛擬出內存,讓每個進程都覺得整個體系中只要自己一個程序在運轉。
這是個好問題。這個問題涉及到虛擬內存的概念。(虛擬內存或許有歧義,在此處應該便是邏輯地址的意思)
關於每個進程來說,他們都有一個獨立的地址空間。比方A進程的0x400000和B進程的0x400000,都是合理的,可是他們不指向同一個方位。
你或許會疑問,為什麼地址相同,卻不在同一個方位。這便是虛擬地址的概念了,咱們之前說到的0x400000是一個虛擬的地址,要經過轉化才幹變成實在的地址,叫做物理地址。這個轉化咱們能夠讓他依據進程不同而不同,就完成了每個進程有他自己的虛擬空間。
至於這個轉化是怎樣產生的,現在一般經過頁表,前期經過段寄存器。當然再早一點是沒有虛擬內存這個概念的,直接便是物理地址,A和B的0x400000便是同一個當地,就會呈現題主的問題。
假如有人看我再進一步講講頁表吧。
關於從虛擬地址到物理地址的問題,不同的操作體系有不同的完成。我講一個簡化的模型(我僅僅個大二的學生,講的不對的當地還請多多指教)。
首要有兩個東西,一個是物理內存,他是實實在在的,你能夠以為便是你的電腦的內存,給他物理地址,他就能拿到東西。還有一個虛擬地址空間,他是每個進程都有的,操作體系提供給每個進程的。進程自己不在乎給他用地址是啥,他只要能正確的從地址裡拿到想要的內容就行了。
比方A進程想拿0x400000這個地址裡的
內容
。咱們前面說進程只知道
虛擬地址
,可是內容保存在物理內存裡呀,咱們要用
物理地址
到內存裡去找內容。所以問題來了,
怎樣把虛擬地址變成物理地址呢?
咱們能夠用
頁表。
假定咱們的物理地址的規模是0x00~0xFF,總共256個。簡略起見,假定咱們的虛擬地址的規模也是0x00~0xFF。(這兩個的空間巨細沒有必定的聯絡)
咱們還有一個表,這張表總共有16項,咱們把他叫做
頁表。
咱們把咱們的
虛擬地址
分紅兩部分,榜首位和第二位。比方0x13這個地址就分紅0x1和0x3。用前一個0x1到頁表裡去找內容,找到0x2對吧。再和0x3拼起來,就得到了0x23,最終咱們找到的內容便是
物理地址

&
–>
那麼這個是怎樣處理題主的問題的呢?咱們再來看個比如,咱們有兩個進程,他們的頁表是下面這個姿態,左進程找地址0x11,依照上面的找法是不是變成0x21,右進程找地址0x11,是不是找到了0xE1。
&
–>
你或許還會問,這個頁表在哪裡?頁表的值是怎樣寫的?物理地址不夠了怎樣辦?這些問題都很有意思,等進一步的學習就會理解啦。別的,實在的狀況雜亂的多,我這個僅僅十分簡易的版別啦,感受一下思維就好了。
看到樓上越講越深入了,我也加幾句。否則要被diss沒了(逃
段寄存器的概念在我這兒徹底疏忽,由於我首要學的是riscv架構,至於我們電腦上遍及的intel架構,段寄存器仍是繞不過去的前史。
上面講的頁表,都是操作體系創立的。也便是說在操作體系發動前是沒有的。虛擬地址空間是操作體系賦予用戶進程,至於他自己怎樣裝在進去發動運轉那便是另一個故事了。
pe/elf格局中有segment和section的概念,這個是操作體系能裝載程序進入內存的重要協助,樓上的答主講的比我好。

Pin It