转汇编
http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
?
阮一峰的網絡日志???首頁???檔案- 上一篇:加密貨幣的本質????
- 下一篇:Docker?入門教程
分類:
?- 理解計算機
匯編語言入門教程
日期:?2018年1月21日
感謝?贊助本站
?
學習編程其實就是學高級語言,即那些為人類設計的計算機語言。
但是,計算機不理解高級語言,必須通過編譯器轉成二進制代碼,才能運行。學會高級語言,并不等于理解計算機實際的運行步驟。
計算機真正能夠理解的是低級語言,它專門用來控制硬件。匯編語言就是低級語言,直接描述/控制 CPU 的運行。如果你想了解 CPU 到底干了些什么,以及代碼的運行步驟,就一定要學習匯編語言。
匯編語言不容易學習,就連簡明扼要的介紹都很難找到。下面我嘗試寫一篇最好懂的匯編語言教程,解釋 CPU 如何執行代碼。
一、匯編語言是什么?
我們知道,CPU 只負責計算,本身不具備智能。你輸入一條指令(instruction),它就運行一次,然后停下來,等待下一條指令。
這些指令都是二進制的,稱為操作碼(opcode),比如加法指令就是00000011。編譯器的作用,就是將高級語言寫好的程序,翻譯成一條條操作碼。
對于人類來說,二進制程序是不可讀的,根本看不出來機器干了什么。為了解決可讀性的問題,以及偶爾的編輯需求,就誕生了匯編語言。
匯編語言是二進制指令的文本形式,與指令是一一對應的關系。比如,加法指令00000011寫成匯編語言就是 ADD。只要還原成二進制,匯編語言就可以被 CPU 直接執行,所以它是最底層的低級語言。
二、來歷
最早的時候,編寫程序就是手寫二進制指令,然后通過各種開關輸入計算機,比如要做加法了,就按一下加法開關。后來,發明了紙帶打孔機,通過在紙帶上打孔,將二進制指令自動輸入計算機。
為了解決二進制指令的可讀性問題,工程師將那些指令寫成了八進制。二進制轉八進制是輕而易舉的,但是八進制的可讀性也不行。很自然地,最后還是用文字表達,加法指令寫成 ADD。內存地址也不再直接引用,而是用標簽表示。
這樣的話,就多出一個步驟,要把這些文字指令翻譯成二進制,這個步驟就稱為 assembling,完成這個步驟的程序就叫做 assembler。它處理的文本,自然就叫做 aseembly code。標準化以后,稱為 assembly language,縮寫為 asm,中文譯為匯編語言。
每一種 CPU 的機器指令都是不一樣的,因此對應的匯編語言也不一樣。本文介紹的是目前最常見的 x86 匯編語言,即 Intel 公司的 CPU 使用的那一種。
三、寄存器
學習匯編語言,首先必須了解兩個知識點:寄存器和內存模型。
先來看寄存器。CPU 本身只負責運算,不負責儲存數據。數據一般都儲存在內存之中,CPU 要用的時候就去內存讀寫數據。但是,CPU 的運算速度遠高于內存的讀寫速度,為了避免被拖慢,CPU 都自帶一級緩存和二級緩存。基本上,CPU 緩存可以看作是讀寫速度較快的內存。
但是,CPU 緩存還是不夠快,另外數據在緩存里面的地址是不固定的,CPU 每次讀寫都要尋址也會拖慢速度。因此,除了緩存之外,CPU 還自帶了寄存器(register),用來儲存最常用的數據。也就是說,那些最頻繁讀寫的數據(比如循環變量),都會放在寄存器里面,CPU 優先讀寫寄存器,再由寄存器跟內存交換數據。
寄存器不依靠地址區分數據,而依靠名稱。每一個寄存器都有自己的名稱,我們告訴 CPU 去具體的哪一個寄存器拿數據,這樣的速度是最快的。有人比喻寄存器是 CPU 的零級緩存。
四、寄存器的種類
早期的 x86 CPU 只有8個寄存器,而且每個都有不同的用途。現在的寄存器已經有100多個了,都變成通用寄存器,不特別指定用途了,但是早期寄存器的名字都被保存了下來。
- EAX
- EBX
- ECX
- EDX
- EDI
- ESI
- EBP
- ESP
上面這8個寄存器之中,前面七個都是通用的。ESP 寄存器有特定用途,保存當前 Stack 的地址(詳見下一節)。
我們常常看到 32位 CPU、64位 CPU 這樣的名稱,其實指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4個字節。
五、內存模型:Heap
寄存器只能存放很少量的數據,大多數時候,CPU 要指揮寄存器,直接跟內存交換數據。所以,除了寄存器,還必須了解內存怎么儲存數據。
程序運行的時候,操作系統會給它分配一段內存,用來儲存程序和運行產生的數據。這段內存有起始地址和結束地址,比如從0x1000到0x8000,起始地址是較小的那個地址,結束地址是較大的那個地址。
程序運行過程中,對于動態的內存占用請求(比如新建對象,或者使用malloc命令),系統就會從預先分配好的那段內存之中,劃出一部分給用戶,具體規則是從起始地址開始劃分(實際上,起始地址會有一段靜態數據,這里忽略)。舉例來說,用戶要求得到10個字節內存,那么從起始地址0x1000開始給他分配,一直分配到地址0x100A,如果再要求得到22個字節,那么就分配到0x1020。
這種因為用戶主動請求而劃分出來的內存區域,叫做 Heap(堆)。它由起始地址開始,從低位(地址)向高位(地址)增長。Heap 的一個重要特點就是不會自動消失,必須手動釋放,或者由垃圾回收機制來回收。
六、內存模型:Stack
除了 Heap 以外,其他的內存占用叫做 Stack(棧)。簡單說,Stack 是由于函數運行而臨時占用的內存區域。
請看下面的例子。
int main() {int a = 2; int b = 3; }上面代碼中,系統開始執行main函數時,會為它在內存里面建立一個幀(frame),所有main的內部變量(比如a和b)都保存在這個幀里面。main函數執行結束后,該幀就會被回收,釋放所有的內部變量,不再占用空間。
如果函數內部調用了其他函數,會發生什么情況?
int main() {int a = 2; int b = 3; return add_a_and_b(a, b); }上面代碼中,main函數內部調用了add_a_and_b函數。執行到這一行的時候,系統也會為add_a_and_b新建一個幀,用來儲存它的內部變量。也就是說,此時同時存在兩個幀:main和add_a_and_b。一般來說,調用棧有多少層,就有多少幀。
等到add_a_and_b運行結束,它的幀就會被回收,系統會回到函數main剛才中斷執行的地方,繼續往下執行。通過這種機制,就實現了函數的層層調用,并且每一層都能使用自己的本地變量。
所有的幀都存放在 Stack,由于幀是一層層疊加的,所以 Stack 叫做棧。生成新的幀,叫做"入棧",英文是 push;棧的回收叫做"出棧",英文是 pop。Stack 的特點就是,最晚入棧的幀最早出棧(因為最內層的函數調用,最先結束運行),這就叫做"后進先出"的數據結構。每一次函數執行結束,就自動釋放一個幀,所有函數執行結束,整個 Stack 就都釋放了。
Stack 是由內存區域的結束地址開始,從高位(地址)向低位(地址)分配。比如,內存區域的結束地址是0x8000,第一幀假定是16字節,那么下一次分配的地址就會從0x7FF0開始;第二幀假定需要64字節,那么地址就會移動到0x7FB0。
七、CPU 指令
7.1 一個實例
了解寄存器和內存模型以后,就可以來看匯編語言到底是什么了。下面是一個簡單的程序example.c。
int add_a_and_b(int a, int b) { return a + b; } int main() { return add_a_and_b(2, 3); }gcc 將這個程序轉成匯編語言。
$ gcc -S example.c上面的命令執行以后,會生成一個文本文件example.s,里面就是匯編語言,包含了幾十行指令。這么說吧,一個高級語言的簡單操作,底層可能由幾個,甚至幾十個 CPU 指令構成。CPU 依次執行這些指令,完成這一步操作。
example.s經過簡化以后,大概是下面的樣子。
_add_a_and_b:push %ebxmov %eax, [%esp+8] mov %ebx, [%esp+12] add %eax, %ebx pop %ebx ret _main: push 3 push 2 call _add_a_and_b add %esp, 8 ret可以看到,原程序的兩個函數add_a_and_b和main,對應兩個標簽_add_a_and_b和_main。每個標簽里面是該函數所轉成的 CPU 運行流程。
每一行就是 CPU 執行的一次操作。它又分成兩部分,就以其中一行為例。
push %ebx這一行里面,push是 CPU 指令,%ebx是該指令要用到的運算子。一個 CPU 指令可以有零個到多個運算子。
下面我就一行一行講解這個匯編程序,建議讀者最好把這個程序,在另一個窗口拷貝一份,省得閱讀的時候再把頁面滾動上來。
7.2 push 指令
根據約定,程序從_main標簽開始執行,這時會在 Stack 上為main建立一個幀,并將 Stack 所指向的地址,寫入 ESP 寄存器。后面如果有數據要寫入main這個幀,就會寫在 ESP 寄存器所保存的地址。
然后,開始執行第一行代碼。
push 3push指令用于將運算子放入 Stack,這里就是將3寫入main這個幀。
雖然看上去很簡單,push指令其實有一個前置操作。它會先取出 ESP 寄存器里面的地址,將其減去4個字節,然后將新地址寫入 ESP 寄存器。使用減法是因為 Stack 從高位向低位發展,4個字節則是因為3的類型是int,占用4個字節。得到新地址以后, 3 就會寫入這個地址開始的四個字節。
push 2第二行也是一樣,push指令將2寫入main這個幀,位置緊貼著前面寫入的3。這時,ESP 寄存器會再減去 4個字節(累計減去8)。
7.3 call 指令
第三行的call指令用來調用函數。
call _add_a_and_b上面的代碼表示調用add_a_and_b函數。這時,程序就會去找_add_a_and_b標簽,并為該函數建立一個新的幀。
下面就開始執行_add_a_and_b的代碼。
push %ebx這一行表示將 EBX 寄存器里面的值,寫入_add_a_and_b這個幀。這是因為后面要用到這個寄存器,就先把里面的值取出來,用完后再寫回去。
這時,push指令會再將 ESP 寄存器里面的地址減去4個字節(累計減去12)。
7.4 mov 指令
mov指令用于將一個值寫入某個寄存器。
mov %eax, [%esp+8]這一行代碼表示,先將 ESP 寄存器里面的地址加上8個字節,得到一個新的地址,然后按照這個地址在 Stack 取出數據。根據前面的步驟,可以推算出這里取出的是2,再將2寫入 EAX 寄存器。
下一行代碼也是干同樣的事情。
mov %ebx, [%esp+12]上面的代碼將 ESP 寄存器的值加12個字節,再按照這個地址在 Stack 取出數據,這次取出的是3,將其寫入 EBX 寄存器。
7.5 add 指令
add指令用于將兩個運算子相加,并將結果寫入第一個運算子。
add %eax, %ebx上面的代碼將 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到結果5,再將這個結果寫入第一個運算子 EAX 寄存器。
7.6 pop 指令
pop指令用于取出 Stack 最近一個寫入的值(即最低位地址的值),并將這個值寫入運算子指定的位置。
pop %ebx上面的代碼表示,取出 Stack 最近寫入的值(即 EBX 寄存器的原始值),再將這個值寫回 EBX 寄存器(因為加法已經做完了,EBX 寄存器用不到了)。
注意,pop指令還會將 ESP 寄存器里面的地址加4,即回收4個字節。
7.7 ret 指令
ret指令用于終止當前函數的執行,將運行權交還給上層函數。也就是,當前函數的幀將被回收。
ret可以看到,該指令沒有運算子。
隨著add_a_and_b函數終止執行,系統就回到剛才main函數中斷的地方,繼續往下執行。
add %esp, 8上面的代碼表示,將 ESP 寄存器里面的地址,手動加上8個字節,再寫回 ESP 寄存器。這是因為 ESP 寄存器的是 Stack 的寫入開始地址,前面的pop操作已經回收了4個字節,這里再回收8個字節,等于全部回收。
ret最后,main函數運行結束,ret指令退出程序執行。
八、參考鏈接
- Introduction to reverse engineering and Assembly, by Youness Alaoui
- x86 Assembly Guide, by University of Virginia Computer Science
(完)
文檔信息
- 版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)
- 發表日期:?2018年1月21日
?
相關文章
- 2018.10.16:?exFAT 文件系統指南 國慶假期,我拍了一些手機視頻,打算存到新買的移動硬盤。
- 2018.07.16:?CAP 定理的含義 分布式系統(distributed system)正變得越來越重要,大型網站幾乎都是分布式的。
- 2018.05.09:?根域名的知識 域名是互聯網的基礎設施,只要上網就會用到。
- 2018.01.11:?加密貨幣的本質 現在,各種加密貨幣(cryptocurrency)不計其數。
廣告(購買廣告位)
Go 語言課程
React 框架課程
留言(95條)
以前在學校的時候看過王爽的匯編語言第二版,那個時候還是很喜歡一些偏底層的東西的,計算機原理等書。現在接觸到高級語言之后就全忘了。。。
2018年1月21日 19:40?|?#?|?引用
阮老師JS的閉包是不是在棧中的內存占用不回收呢?會一直占用?還是說閉包的空間是v8申請的椎的空間?
2018年1月21日 20:25?|?#?|?引用
平時接觸不到這些細節,但非常喜歡這方面的知識,懇請阮老師推薦一些這方面的權威書籍。;)
2018年1月21日 22:23?|?#?|?引用
感謝分享
2018年1月22日 02:09?|?#?|?引用
為什么我們用到EBX就push EBX,而用到EAX卻沒push EAX呢?
2018年1月22日 08:01?|?#?|?引用
@jimmy
我的理解是 EAX 屬于最頻繁使用的通用寄存器,所以約定沒有必要保留它的值。
2018年1月22日 09:36?|?#?|?引用
@kailin:參考鏈接里面,我已經提供了兩篇文章。
@zeon:是的,閉包屬于 Stack 里面的幀不回收,詳見?https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec
2018年1月22日 09:38?|?#?|?引用
基本的點都講到了,但不是計算機專業的讀者估計還是會有點懵,建議結合下內存總線和硬件指令的電路原理。
2018年1月22日 09:41?|?#?|?引用
關注阮大有一陣時間了,從來沒留過言,剛好最近在重新看王爽的《匯編語言》,沒想到阮大最近發布了這篇文章,必須手動來贊了!
2018年1月22日 10:17?|?#?|?引用
阮老師牛逼!阮老師,這個匯編語言現在市場上一般企業這方面人的需求不大吧?
2018年1月22日 10:31?|?#?|?引用
== 它會先取出 ESP 寄存器里面的地址,將其減去4個字節,然后將新地址寫入 ESP 寄存器。使用減法是因為 Stack 從高位向低位發展,4個字節則是因為3的類型是int,占用4個字節。得到新地址以后, 3 就會寫入這個地址開始的四個字節。==
這個地方不懂啊
2018年1月22日 10:58?|?#?|?引用
引用jimmy的發言:
為什么我們用到EBX就push EBX,而用到EAX卻沒push EAX呢?
EAX用于保存返回值,這個值肯定會被覆蓋,所以需要調用著保存。
rbx,rbp,r12-r15是被調用著保存寄存器,如果被調用著需要使用,就需要壓入stack中。
其他寄存器是調用者保存寄存器,調用者如果需要調用前后這些值保持一致,則需要自己保存起來
2018年1月22日 11:41?|?#?|?引用
可以看看 小甲魚的匯編教程視頻
2018年1月22日 12:04?|?#?|?引用
intel格式的匯編寄存器之前不用加%吧
2018年1月22日 12:41?|?#?|?引用
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _add_a_b
.p2align 4, 0x90
_add_a_b: ## @add_a_b
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi
movl %esi, %eax
popq %rbp
retq
.cfi_endproc
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi3:
.cfi_def_cfa_offset 16
Lcfi4:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi5:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $1, %edi
movl $2, %esi
movl $0, -4(%rbp)
callq _add_a_b
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
2018年1月22日 13:24?|?#?|?引用
引用阮老師小迷弟的發言:
阮老師牛逼!阮老師,這個匯編語言現在市場上一般企業這方面人的需求不大吧?
一般開發中確實用不到,也很少有人會用到匯編去做開發。但是如果你不了解匯編,就沒有辦法真正了解Java虛擬機中的各種概念。只有真正學習過匯編的人,才能真正理解各種概念
2018年1月22日 15:27?|?#?|?引用
引用xxxx的發言:
一般開發中確實用不到,也很少有人會用到匯編去做開發。但是如果你不了解匯編,就沒有辦法真正了解Java虛擬機中的各種概念。只有真正學習過匯編的人,才能真正理解各種概念
不用理解JAVA虛擬機的概念。造汽車的人,無需了解鋼鐵是如何煉成的。
很多技術是層疊的,做頂層業務的,是需也不太可能將所有底層技術進行追溯學習的。
2018年1月22日 16:41?|?#?|?引用
0xFFFF0010(_main+16)這里指的是為_add_a_and_b建立的幀嗎?也是占4個字節嗎?那_main+16又是指什么?
2018年1月22日 17:41?|?#?|?引用
一個匯編指令對應的機器碼不一定只有一個。比如,匯編器會根據mov后面的操作數將mov翻譯成不同的機器碼。以前一直想不明白,后來終于在依照51單片機手冊用C寫模擬器時弄明白了。同時也知道了寄存器A、AX、EAX、RAX。
個人還是覺得看看CMU的CSAPP對計算機入門很有幫助,不論平時工作是做哪方面的開發:JS,PHP,Python,Bash,Java,C-like(如果書里的入門內容都不會,估計也做不了C-like開發)。
2018年1月22日 18:22?|?#?|?引用
當初學匯編,被指令搞的頭暈眼花的,習慣了又發現還有32位64位的坑。。
2018年1月22日 18:37?|?#?|?引用
看完阮老師的文章另我想起很久以前做出版時經常出現 postscript 錯誤 stack overflow,
萬分感謝阮老師分享,另我了解底層技術!
2018年1月22日 23:02?|?#?|?引用
為什么一提匯編就要上x86的匯編呢……x86匯編很麻煩的……
比如我一開始用的匯編就是PS2的CPU(EE)的MIPS匯編,覺得真是簡單清晰啊……
2018年1月23日 05:49?|?#?|?引用
全部忘記了,就像失憶一樣。
2018年1月23日 09:33?|?#?|?引用
引用阮老師小迷弟的發言:
阮老師牛逼!阮老師,這個匯編語言現在市場上一般企業這方面人的需求不大吧?
基礎扎實了,什么高級語言搞不定!
2018年1月23日 09:34?|?#?|?引用
intel 格式匯編沒有% AT&T 匯編有% 但是格式和intel 相反
2018年1月23日 20:09?|?#?|?引用
通俗易懂。阮老師帶我們輕松復習了一遍丟掉好久的知識。:-)
2018年1月24日 08:18?|?#?|?引用
講解生動,十分感人,Heap和Stack百度搜過N多次,這是第一次看到結合‘堆’、‘棧’語義的簡單解釋,非常好,以后應該都能記住了。
贊 贊 贊
2018年1月24日 09:49?|?#?|?引用
計算機組成原理...今年剛考完...汗
2018年1月24日 12:52?|?#?|?引用
我請教一個問題,如文中所述:比如,內存區域的結束地址是0x8000,第一幀假定是16字節,那么下一次分配的地址就會從0x7FF0開始;第二幀假定需要64字節,那么地址就會移動到0x7FB0。
我想知道是怎樣分配每一幀的大小呢,比如_add_a_and_b:,我該怎樣知道分配多少內存給這一幀呢?
2018年1月25日 09:59?|?#?|?引用
AT&T 匯編,感覺更舒服,而且目前unix linux 系列支撐
2018年1月25日 14:51?|?#?|?引用
引用阮老師的小粉絲的發言:
我請教一個問題,如文中所述:比如,內存區域的結束地址是0x8000,第一幀假定是16字節,那么下一次分配的地址就會從0x7FF0開始;第二幀假定需要64字節,那么地址就會移動到0x7FB0。
我想知道是怎樣分配每一幀的大小呢,比如_add_a_and_b:,我該怎樣知道分配多少內存給這一幀呢?
?
C語言的數據類型都有大小。編譯器可以根據函數內定義的所有局部變量(其實真實情況更復雜一些,還有static,const等修飾符會影響),一次性“分配”出相應數量的內存(就是將sp的值減去相應的大小)。函數返回時,會先清理掉自己的局部變量(用leave把bp恢復到sp),再將sp所指地址的內容恢復到bp,然后ret。阮老文章里的例子過于簡單,有些指令沒用上。
P.S.
匯編指令也是會分成幾個步驟執行(所謂的指令周期,機器周期,時鐘周期),所以如果有說錯或不清楚的地方,勿噴,多包涵。
2018年1月25日 15:21?|?#?|?引用
請教您一個問題,Heap是先進先出的嗎?我在StackOverflow上看到的是Heap沒有一個明確的頂,所以它可以隨時進入和出去。
2018年1月25日 15:42?|?#?|?引用
引用alexsaurora的發言:
請教您一個問題,Heap是先進先出的嗎?我在StackOverflow上看到的是Heap沒有一個明確的頂,所以它可以隨時進入和出去。
堆棧,堆棧,堆是堆,棧是棧。
(信號是信號,信號量是信號量,一個是signal,一個是semaphore。回想起了好些迷惑的術語翻譯)
=======
進程中heap跟在data區域的后面(請參考任何一個進程的maps)。heap所占用的內存是C庫調用brk系統調用向操作系統(暫時不考慮Windows)申請的(詳細內容可以man brk家族的文檔),操作系統只是維護brk的位置,C庫會負責管理申請到的內存。
以前debug segfault時,看過Android的malloc實現,其實就是Doug Lea的dlmalloc(wiki有詳細介紹,Android源碼也可以隨便看)。dlmalloc會根據程序的需求將操作系統給的連續內存分成內存塊,每個塊的頭部保存著大小、是否已分配等信息。塊是內存對齊的,相鄰塊是緊挨著的(因此,可以合并)。當代碼調用malloc時,malloc會查找滿足需求的塊,如果找不到,就會再次調用brk向操作系統申請。當代碼調用free時,free會更新塊頭部的信息,可能還會把相鄰的空閑塊合并,組成更大的塊。
實際情況比描述的要復雜,并且也有其他實現方式。dlmalloc的數據結構和擴展分區的結構很類似。從第一個塊開始捋,順藤摸瓜,就可以遍歷所有塊。我嘴比較笨,描述不清楚,網上有很多形象的圖片可以看。
2018年1月25日 18:22?|?#?|?引用
感謝分享,學過微機原理,沒用上
2018年1月26日 17:17?|?#?|?引用
引用zeon的發言:
阮老師JS的閉包是不是在棧中的內存占用不回收呢?會一直占用?還是說閉包的空間是v8申請的椎的空間?
JS里的閉包,都是在堆中申請的,由GC管理,不是這里的棧,“JS棧”與匯編或C語言中的棧是兩個概念。匯編棧不存在GC,由函數調用與返回來自動更新SP指針實現的。JS函數與這兒的函數是兩種東西。
2018年1月28日 17:49?|?#?|?引用
忘的差不多了。
2018年1月29日 12:19?|?#?|?引用
這一句話是錯誤的:“32 位 CPU 的寄存器大小就是4個字節”。32位CPU容量是可拓展的,可修正為32位CPU的的最大尋址范圍是4G。
2018年1月30日 09:41?|?#?|?引用
阮老師好像有10天的時間沒更新文章了!
2018年1月31日 10:21?|?#?|?引用
阮老師,您好,我是將在今年畢業的一名大學生,對前端很感興趣,希望你能給我學習前端的建議的大概方向書籍網站資源等。我看過了你的JavaScript標準參考教程,覺得寫得很通俗易懂。希望您能看到,等待您的寶貴建議。
2018年2月 1日 16:26?|?#?|?引用
想知道這個留言系統
2018年2月 2日 09:46?|?#?|?引用
引用jimmy的發言:
為什么我們用到EBX就push EBX,而用到EAX卻沒push EAX呢?
好久沒看匯編,忘得差不多了。
好像是eax里面一般保存的是返回值,所以執行過程中ebx寄存器需要先把內容壓棧,
使用完恢復,eax則不用,因為最終的返回值就在里面。
2018年2月 2日 18:11?|?#?|?引用
引用張春星的發言:
== 它會先取出 ESP 寄存器里面的地址,將其減去4個字節,然后將新地址寫入 ESP 寄存器。使用減法是因為 Stack 從高位向低位發展,4個字節則是因為3的類型是int,占用4個字節。得到新地址以后, 3 就會寫入這個地址開始的四個字節。==
這個地方不懂啊
ESP始終指向棧頂,棧從高地址向低地址增長,push 2,push 3分別把兩個
參數壓入棧中,此時的棧頂的指針因為壓入兩個4字節的Int類型,指向初始地址
減8的內存單元,然后函數調用返回后,add esp,8就是直接設置esp指向的位置,
進行加8操作后,棧頂指針回到函數調用前的位置,這個叫棧平衡,然后好像是intel,
還是windowis,他們的編譯器規定函數調用,由調用者恢復棧平衡,所以最后是main函數部分做加8這個操作。
2018年2月 2日 18:18?|?#?|?引用
那,printf 那種的函數會被編譯成什么呢?
2018年2月 4日 23:18?|?#?|?引用
引用zeon的發言:
阮老師JS的閉包是不是在棧中的內存占用不回收呢?會一直占用?還是說閉包的空間是v8申請的椎的空間?
建議了解瀏覽器內存回收機制。閉包是因為一直保持引用關系,所以不會被回收
2018年2月 5日 09:38?|?#?|?引用
阮老師的文章很贊
2018年2月 6日 17:15?|?#?|?引用
引用zhanghang的發言:
計算機組成原理...今年剛考完...汗
你好,可能理解力不夠。我還是沒看懂的一點是,匯編語言只是二進制的文本形式,那最后計算機是直接識別運行這個匯編代碼嗎?感覺還差一步,就是匯編語言 -> 機器碼
2018年2月 6日 17:54?|?#?|?引用
為啥稱呼為x86
2018年2月 7日 21:51?|?#?|?引用
mov %eax, [%esp+8]?
mov %ebx, [%esp+12]
怎么感覺應該是
mov %eax, [%esp+4]?
mov %ebx, [%esp+8]
2018年2月 8日 01:24?|?#?|?引用
引用Silen的發言:
mov%eax, [%esp+8]?
mov%ebx, [%esp+12]
怎么感覺應該是
mov%eax, [%esp+4]?
mov%ebx, [%esp+8]
?
同問,比較疑惑,如果push寫入棧時是從低位開始寫,那讀取應該也是從低位開始讀吧?那地址應該是?
[%esp+4](取到2),?
[%esp+8](取到3)?
這樣吧?
2018年2月 8日 17:57?|?#?|?引用
引用rus的發言:
?
同問,比較疑惑,如果push寫入棧時是從低位開始寫,那讀取應該也是從低位開始讀吧?那地址應該是?
[%esp+4](取到2),?
[%esp+8](取到3)?
這樣吧?
%ebx 占了4個字節
2 本身占了4個字節
讀取數據時得返回到數據開始的位置
是這樣吧?
2018年2月 8日 23:43?|?#?|?引用
阮大寫的很好,非計算機專業的我也能理解,很缺乏這方面知識,看完學會了很多,感謝.
2018年2月11日 00:18?|?#?|?引用
阮大佬寫的東西總是那么通俗易懂
在這不光學到了知識,平常寫文章的思路也清晰了
-.-
2018年2月27日 13:30?|?#?|?引用
大神
量子計算機的底層語言方面的能來一篇么。
2018年3月 4日 09:41?|?#?|?引用
"得到結果5,再將這個結果寫入第一個運算子 EAX 寄存器"
結果是如何返回的呢?阮老師。感覺EBX寄存器被臨時拿來用恢復原樣,但EAX寄存器存的固定的是內存全部回收后返回值么?
2018年3月 8日 11:25?|?#?|?引用
引用GD的發言:
?
%ebx 占了4個字節
2本身占了4個字節
讀取數據時得返回到數據開始的位置
是這樣吧?
?
[%esp]--> %ebx?
[%esp+4] --> call 語句的下一條指令的地址
[%esp+8] --> 第一個參數
[%esp+12] --> 第二個參數
正是因為[%esp+4] 是call 語句的下一條指令地址,才會有Stack OverFlow。
2018年3月 8日 17:13?|?#?|?引用
期待下一講。百度一大堆教程,沒幾個看得懂的,而且頭疼。
阮老師這一講精彩,期待更多內容
2018年3月10日 20:47?|?#?|?引用
引用kailin’的發言:
平時接觸不到這些細節,但非常喜歡這方面的知識,懇請阮老師推薦一些這方面的權威書籍。;)
https://item.jd.com/12006637.html?可以看這本書
2018年3月14日 12:02?|?#?|?引用
阮老師,我正在學匯編,突然看到這篇文章讓我對匯編又有了更深的理解。但是我發現老師你文中的匯編代碼是 Intel 風格和 AT&T 風格的混用體
2018年3月19日 15:32?|?#?|?引用
同問,
mov%eax, [%esp+8]?
mov%ebx, [%esp+12]
為什么不是
mov%eax, [%esp+4]?
mov%ebx, [%esp+8]
2018年3月27日 16:39?|?#?|?引用
看了很多文章都不太通透,當然現在還是不太通透,但比過去好太多了,老師的講解的很細致,非常感謝。
2018年3月27日 20:54?|?#?|?引用
樓主寫的太好了,簡單易懂,高人。
2018年3月29日 10:16?|?#?|?引用
舉例來說,用戶要求得到10個字節內存,那么從起始地址0x1000開始給他分配,一直分配到地址0x100A,如果再要求得到22個字節,那么就分配到0x1020
這里沒看懂,22個字節不是應該分配到0x1016嗎?,我感覺32個字節才應該是0x1020
2018年3月30日 01:18?|?#?|?引用
六、內存模型:Stack
這里也不太懂后進先出,main和add_a_and_b,我覺得后進棧的應該是add_a and b,那么先出的也應該是它,但是從圖看感覺是main先完成的,然后再進行的add_a and b,這里就很懵了,希望大哥們指點迷津
2018年3月30日 01:34?|?#?|?引用
寫的很不錯
2018年4月 8日 19:59?|?#?|?引用
引用William_ch的發言:
?
[%esp]--> %ebx?
[%esp+4] --> call 語句的下一條指令的地址
[%esp+8] --> 第一個參數
[%esp+12] --> 第二個參數
正是因為[%esp+4] 是call 語句的下一條指令地址,才會有Stack OverFlow。
我理解其實應該是因為+4的位置放了一條函數指針,也就是返回main函數的入口,指針存放的是地址,大小4字節,所以數據2和3對應的是+8和+12,不知道這樣理解是否正確,還請指正,謝謝~
2018年4月12日 10:08?|?#?|?引用
有點疑問:
push %ebx
mov %eax, [%esp+8]?
mov %ebx, [%esp+12]
這里ebx存的是什么?是push esp的那兩個參數嗎?
那在函數_add_a_and_b下
mov %eax, [%esp+8] 和?
mov %ebx, [%esp+12] 為什么不是減少地址而是加?
2018年4月13日 12:18?|?#?|?引用
引用shine的發言:
有點疑問:
push %ebx
mov%eax, [%esp+8]?
mov%ebx, [%esp+12]
這里ebx存的是什么?是push esp的那兩個參數嗎?
那在函數_add_a_and_b下
mov%eax, [%esp+8] 和
mov%ebx, [%esp+12] 為什么不是減少地址而是加?
?
ebx原來保存的數據先取出來,防止原數據被覆蓋;
加是因為棧區是從高位地址開始分配,esp當前保存的總是低地址
2018年4月25日 11:31?|?#?|?引用
關于+4 +8的,我覺得其實其中還有一塊存放著上一個函數的return地址(32位地址占4個) 所以是+8去取參數2的開頭地址。
2018年5月 6日 22:10?|?#?|?引用
請教一下這些圖片是用什么軟件畫出來的?
2018年5月30日 15:44?|?#?|?引用
阮老師講的很通俗易懂,思路清晰,我一個計算機原理小白都看的一知半解的了,希望以后有更多這樣的文章
2018年6月 1日 16:58?|?#?|?引用
從零學匯編,只有會這方面的人才懂得我這份熱情
2018年6月11日 13:12?|?#?|?引用
引用阮一峰的發言:
@jimmy
我的理解是 EAX 屬于最頻繁使用的通用寄存器,所以約定沒有必要保留它的值。
哈哈,我覺得是因為程序中的return,假設把原程序修改一下,改成:
int add_a_and_b(int a, int b) {
return a + b;
}
int main() {
return add_a_and_b(2, 3) + 3; // 修改此處代碼
}
那么,在_add_a_and_b函數返回時,就不需要再從內存中取出結果了,直接把后面累加的3存到EBX中,然后執行add %eax, %ebx即可。
這樣就少了一次從內存中取出數據的操作(可能還有存入內存的操作)。
2018年6月11日 17:58?|?#?|?引用
第一個問題:
為什么Stack要設計成從高位到低位?
這樣做,每次向Stack內執行push的時候,ESP都要執行減法操作,這樣性能難道比"從低位到高?
位,每次執行加法操作"更好?
還是說,因為為了讀取數據的時候執行加法( [%esp+8] )而做出的優化?
第二個問題:
計算機是怎么知道3存在0x0000到0x0004之間,2存在0x0005到0x0008之間?
畢竟ESP只是記住了當前Stack所存儲的數據的最低位地址!
難道是寫死在程序里的?就像例子中的
mov %eax, [%esp+8]?
mov %ebx, [%esp+12]
一樣,程序已經寫死了偏移量?
如果我的問題描述不夠明確,您可以通過郵箱聯系我,這兩個問題我很想知道
2018年6月11日 18:19?|?#?|?引用
講得真好! 不過,語句講得太少了。
2018年6月21日 16:17?|?#?|?引用
棒,看了很多講解匯編原理的,大都晦澀難懂,唯有這篇看了以后令人茅塞頓開
2018年7月19日 00:43?|?#?|?引用
我想請問一下為什么我的GCC編譯出來的匯編指令全是movl popq 以及.cfi_startproc之類的很復雜的指令,與老師的大不相同?
2018年7月21日 00:11?|?#?|?引用
引用kimika的發言:
我想請問一下為什么我的GCC編譯出來的匯編指令全是movl popq 以及.cfi_startproc之類的很復雜的指令,與老師的大不相同?
因為你用的是64位的(q代表以4字節為單位操作),必需在gcc后面加個“-m32”
2018年7月24日 23:46?|?#?|?引用
原來留言不可以超過1200字……我只好分開發了……
(↑↑↑上面的xxx其實也是我……)
只能說阮老師簡化得太多了,我表示很遺憾……
那我盡量講清楚吧
開始寫這個已經半夜了……如果以上有錯誤其實很正常,請立即指出但要多多包涵。
引用Singu的發言:第一個問題:
為什么Stack要設計成從高位到低位?
這樣做,每次向Stack內執行push的時候,ESP都要執行減法操作,這樣性能難道比"從低位
到高?
位,每次執行加法操作"更好?
還是說,因為為了讀取數據的時候執行加法( [%esp+8] )而做出的優化?
第二個問題:
計算機是怎么知道3存在0x0000到0x0004之間,2存在0x0005到0x0008之間?
畢竟ESP只是記住了當前Stack所存儲的數據的最低位地址!
難道是寫死在程序里的?就像例子中的
mov%eax, [%esp+8]?
mov%ebx, [%esp+12]
一樣,程序已經寫死了偏移量?
如果我的問題描述不夠明確,您可以通過郵箱聯系我,這兩個問題我很想知道
這兩個問題被問了很多遍啊……我想解決它……
能一起提出這兩個問題的人很厲害啊,因為這兩個問題是相互關聯的!!!
2018年7月25日 01:30?|?#?|?引用
以32位x86為例:
信息1. call [addr]等價于push %eip加jmp [addr],無非%eip是不能直接操作的(就是說
push %eip是無效指令)
(注:%eip是存儲下一條指令地址的寄存器)
信息2. push [data]等價于sub sizeof(data),%esp加mov [data],[%esp],也就是說,在
push時%esp只能減小,這是歷史遺留問題……(你要自己設計cpu當然可以定義為增加)
信息3. ret(無參)等價于pop %eip(有跳轉效果,因為直接修改%eip相當于跳轉,jmp指
令內部原理就是修改%eip)
信息4. 對于現代32位x86,正確(完整且沒經過優化的)函數應該大概長這樣:
// gcc -S test.cpp -o test.s -m32 (64位機器一定要加-m32,指定使用32位
)
void foo(int,int);
int test() {
int a = 3;
int b = 2;
foo(2,3);
foo(a,b);
return 999;
}
(注:這是真實的gcc輸出)
(詳見下一條)
2018年7月25日 01:30?|?#?|?引用
__Z4testv:(使用一般調用協議,詳情請查詢“ABI”,有歷史遺留問題
)
pushl %ebp(保存調用者設置的%ebp)
movl %esp, %ebp(保存調用者設置的%esp)
(注意:調用者的%esp被保存%ebp里,以上兩句指令可以縮寫為enter,
這是另一個指令)
(從此以后%ebp成為棧空間尋址的基準,因此%ebp全稱為(擴展)基址指
針寄存器)
(問:根據信息1和2,在%ebp - 4上的是什么東西?)
(答:返回地址)
(問:那么在%ebp - 8上的是什么東西?)
(答:保存的%esp)
(問:那么,在%ebp上的是什么東西?)
(答:不知道~這個地址原則上不可以訪問,它屬于調
用者的棧空間)
(問:那么%ebp - 12呢?)
(答:變量a,見下)
subl $40, %esp(為call預留%esp,也就是棧空間,共計40字節,由
編譯器計算得)
(這個40很復雜,用于保存局部變量、傳出的參數等,不展開,因為這個
由編譯器決定)
(40字節不是都被使用了,因為我們并沒有開優化,你能否計算出有多少
字節沒被使用?)
(答:有16字節被浪費了,16=40-8-sizeof(a)-sizeof(b)-
sizeof(傳出參數消耗的棧),詳情見下)
movl $3, -12(%ebp)(預留了就可以使用這種方法保存變量,這個相當于int a
= 3)
movl $2, -16(%ebp)(相當于int b = 2,具體地址由編譯器分配
)
(還沒完,見下一條)
2018年7月25日 01:31?|?#?|?引用
movl $3, 4(%esp)(這里還是被編譯器優化過了,相當于
push %eax,見信息2,mov比push快,無非棧空間必需夠大)
(注:別忘了esp=ebp-40)
movl $2, (%esp)(所以這兩句話相當于先push $3,再push $2)
call __Z3fooii(干一點其他事情,不用管棧空間變量,因為棧空間比%esp地
址高,被保護)
movl -16(%ebp), %eax(先從棧空間加載數據到%eax寄存器)
movl %eax, 4(%esp)(再把%eax里的數據轉入%esp,因為mov指令一次只能操作
一個單位的內存)
(拓展:局部變量的賦值如果不開優化也是類似這么寫的)
movl -12(%ebp), %eax
movl %eax, (%esp)
call __Z3fooii
movl $999, %eax (設置返回值,返回值存在%eax里,歷史遺留問題)
leave (恢復調用者的%ebp和%esp,這句指令也有另一種寫法,大家可以思考一下
)
ret (真正的返回,思考:根據信息3,為什么這樣用是安全的,棧不是被動過了
嗎?)
ps: 是不是覺得編譯器超強大(我還沒開優化呢……)
ps: 軟件底層概念超多,極耗腦力,還有極多歷史遺留問題,這種問題真的很難一次性給答
案……我已經盡力了,打字打的我累死
2018年7月25日 01:32?|?#?|?引用
其實我覺得阮老師對匯編的掌握也不夠多(或許只是文章篇幅太短?)同學們別問了……(阮老師要再想寫這方面的文章,講真的可以聯系我)
(我發的東西格式是不是出了點問題……)
2018年7月25日 01:38?|?#?|?引用
有一個疑問,esp寄存器中存放的函數的地址是起始地址和結束地址還是只是結束地址呢?不好意思哦,可能問題有點白癡。
2018年8月 5日 23:49?|?#?|?引用
引用Singu的發言:
第一個問題:
為什么Stack要設計成從高位到低位?
這樣做,每次向Stack內執行push的時候,ESP都要執行減法操作,這樣性能難道比"從低位到高?
位,每次執行加法操作"更好?
還是說,因為為了讀取數據的時候執行加法( [%esp+8] )而做出的優化?
第二個問題:
計算機是怎么知道3存在0x0000到0x0004之間,2存在0x0005到0x0008之間?
畢竟ESP只是記住了當前Stack所存儲的數據的最低位地址!
難道是寫死在程序里的?就像例子中的
mov%eax, [%esp+8]?
mov%ebx, [%esp+12]
一樣,程序已經寫死了偏移量?
如果我的問題描述不夠明確,您可以通過郵箱聯系我,這兩個問題我很想知道
第一個問題, 因為heap從低到高分配性能高,stack只能從高到低嘍,地址兩頭往中間拱,空間利用率高啊
第二個問題, 什么偏移量,什么寫死,32位系統,除8,就是4啊
個人理解,歡迎拍
2018年9月20日 11:20?|?#?|?引用
寫得真好,期待第二課
2018年9月21日 17:04?|?#?|?引用
確實學到知識了,謝謝~
2018年9月30日 10:17?|?#?|?引用
引用齊軍的發言:
第二個問題, 什么偏移量,什么寫死,32位系統,除8,就是4啊
第二個問題,我覺得是編譯器寫死的
2018年10月 9日 13:50?|?#?|?引用
太感謝了,前段時間想學匯編,看朱邦復老先生的《組合語言的藝術》,看不太懂,他還推薦《ZEN of Assembly Language》
2018年10月16日 19:39?|?#?|?引用
引用阮一峰的發言:
@jimmy
我的理解是 EAX 屬于最頻繁使用的通用寄存器,所以約定沒有必要保留它的值。
南大的《計算機系統基礎》上說,每個指令集體系有一個約定,規定哪些寄存器是調用者保存,哪些寄存器由被調用者來保存
2018年10月23日 14:29?|?#?|?引用
「幀」的英文叫什么?segment?
2018年12月12日 23:08?|?#?|?引用
高級語言學的不少,但是對內存、CPU對程序的執行還是不了解,所以,看看匯編確實受益匪淺,感謝!!!
2018年12月27日 18:54?|?#?|?引用
引用Silen的發言:
mov%eax, [%esp+8]?
mov%ebx, [%esp+12]
怎么感覺應該是
mov%eax, [%esp+4]?
mov%ebx, [%esp+8]
?
第一天看匯編,看了阮老師提供的英文鏈接,在調用函數“ _add_a_and_b"時,首先會在stack中保存main函數中下一條指令“ add %esp, 8”的地址,距離“_main”有4條指令,所以是“_main+4*4”即“_main+16”。此時
ESP 寄存器會再減去 4個字節(累計減去12)。
然后再執行子函數中的“push %ebx”這時,push指令會再將 ESP 寄存器里面的地址減去4個字節(累計減去16)。所以感覺阮老師在文中此處的解釋應該有誤(原文解釋此時累計減去12)。
所以
mov%eax, [%esp+8] 對應數據“2”
mov%ebx, [%esp+12] 對應數據“3”
2019年1月22日 17:35?|?#?|?引用
引用立猛的發言:
intel 格式匯編沒有% AT&T 匯編有% 但是格式和intel 相反
是的,阮老師的這篇文章寫的有問題。
2019年2月24日 17:13?|?#?|?引用
我覺得通過匯編來深刻理解高級語言的原理不是最大的作用,最大的作用應該就是反匯編寫外掛了....,要學寫外掛,匯編是第一個要掌握的東西
2019年2月25日 10:10?|?#?|?引用
引用hazdzz的發言:
「幀」的英文叫什么?segment?
frame吧
2019年2月26日 18:46?|?#?|?引用
我要發表看法
您的留言 (HTML標簽部分可用)
您的大名:
??-必填
電子郵件:
??-必填,不公開
個人網址:
??-我信任你,不會填寫廣告鏈接
記住個人信息?
??- 點擊按鈕
微博?|?推特?|?GitHub
2019 ??我的郵件?|
轉載于:https://www.cnblogs.com/maweiwei/p/10499530.html
總結
- 上一篇: 通过Ajax来简单的实现局部刷新(主要为
- 下一篇: 边分治学习笔记