C指针原理(43)-helloworld的C程序汇编剖析
一、匯編基礎
1、指令碼與數據處理
當計算機處理應用程序運行指令碼時,數據指針指示處理器如何在內存的數據區域尋找要處理的數據,這塊區域也稱為堆棧,指令碼放在另外的指令區,此外,還有指令指針機制,當處理器完成一個指令碼的處理后,指令指針指向下一條指令碼。
IA-32指令碼(INTEL、AMD公司的CPU使用)由一堆二進制碼構成,其格式為:
指令前綴、操作碼、可選修飾符、可選數據元素
指令前綴可包含1到4個修改操作碼行為的1字節前綴,分為:
鎖定前綴和重復前綴
段覆蓋前綴和分支提示前綴
操作數長度覆蓋前綴
地址長度覆蓋前綴
操作碼定義了處理器執行的功能
修飾符定義執行功能時涉及的寄存器和內存位置。
數據元素是完成功能需要使用的數據,這些數據可以是直接的數據值,也可以是數據在內存中的地址。
2、匯編語言
以LINUX/UNIX環境下的匯編語言AT&T匯編(WINDOWS下有一種常用的匯編格式Intel匯編)進行講解,匯編語言允許程序員方便地創建指令碼程序,但不是用那些二進制編碼的格式,還是使用助記符,助記符使用不同的詞表示不同的指令碼,有了助記符,程序員可以用英語來書寫在目標機器上執行的指令碼,不用記憶那些無趣的二進制編碼。
通常, FreeBSD 的內核使用 C 語言的調用規范。 此外, 雖然我們使用 int $0x80來訪問內核, 但是我們常常通過調用一個函數來執行 int $0x80, 而不是直接訪問。這個規范是非常方便的, 比 Microsoft� 的 MS-DOS上使用的規范更加優越。 為什么呢? 因為 UNIX� 的規范允許任何語言所寫的程序訪0問內核。這意味著在freebsd下訪問內核需要先將參數壓入棧中,然后再執行 int $0x80調用內核中斷,執行內核函數。下面這段代碼是經典的helloworld匯編代碼:
dp@dp:~ % vim helloworld.s
#hello.s
.data # 數據段聲明
msg : .string "Hello, world!\n" # 要輸出的字符串len = . - msg # 字串長度.text # 代碼段聲明
.global _start # 指定入口函數
_start: # 在屏幕上顯示一個字符串
pushl $len # 參數三:字符串長度pushl $msg # 參數二:要顯示的字符串pushl $1 # 參數一:文件描述符(stdout) movl $4, %eax # 系統調用號(sys_write) pushl %eaxint $0x80 # 調用內核功能# 退出程序movl $0,%ebx # 參數一:退出代碼movl $1,%eax # 系統調用號(sys_exit) int $0x80 # 調用內核功能在LINUX/UNIX(以freebsd為例)下,可以使用gs和ld軟件匯編和鏈接。
dp@dp:~ % as -o helloworld.o helloworld.s
dp@dp:~ % ld -o helloworld helloworld.o
dp@dp:~ % ./helloworld
Hello, world!
dp@dp:~ %
也可以直接使用GCC命令編譯,但用gcc編譯時將入口函數名由_start改為main。
dp@dp:~ % vim helloworld.s
#hello.s
.data # 數據段聲明
msg : .string "Hello, world!\n" # 要輸出的字符串len = . - msg # 字串長度.text # 代碼段聲明
.global main # 指定入口函數
main: # 在屏幕上顯示一個字符串
pushl $len # 參數三:字符串長度pushl $msg # 參數二:要顯示的字符串pushl $1 # 參數一:文件描述符(stdout) movl $4, %eax # 系統調用號(sys_write) pushl %eaxint $0x80 # 調用內核功能# 退出程序movl $0,%ebx # 參數一:退出代碼movl $1,%eax # 系統調用號(sys_exit) int $0x80 # 調用內核功能~
匯編后運行
dp@dp:~ % gcc -o helloworld helloworld.s
dp@dp:~ % ./helloworld
Hello, world!
dp@dp:~ %
對于ubuntu等Linux 是一個類 UNIX 操作系統。 但是, 它的內核在傳遞參數的時候, 使用和 MS-DOS 相同系統調用規范。 比如在 UNIX 的規范中, 代表內核函數的數字存放在 EAX 中。 但是在 Linux 中, 參數不進行壓棧而是存放在 EBX, ECX, EDX, ESI, EDI, EBP。因此在ubuntu下這段代碼需要這樣編寫(設使用GCC編譯)
#hello.s
.data # 數據段聲明
msg : .string "Hello, world!\n" # 要輸出的字符串len = . - msg # 字串長度ext # 代碼段聲明
global main # 指定入口函數
main: # 在屏幕上顯示一個字符串
movl $len, %edx # 參數三:字符串長度movl $msg, %ecx # 參數二:要顯示的字符串movl $1, %ebx # 參數一:文件描述符(stdout) movl $4, %eax # 系統調用號(sys_write) int $0x80 # 調用內核功能# 退出程序movl $0,%ebx # 參數一:退出代碼movl $1,%eax # 系統調用號(sys_exit) int $0x80 # 調用內核功能2、C語言編譯
C語言屬于高級語言,對匯編語言程序員來說也許是一種解脫,完成同樣的任務,程序編碼量減少很多,這只是把很多編碼生成轉移除到了編譯器上來而已,讓機器承擔這部分編碼生成工作。以freebsd系統、intel 的Intel? Pentium?CPU為例,編寫以下hello,world代碼
dp@dp:~ % cat hello.c
include <stdio.h>
int main(){
printf(“hello,world\n”);
return 0;
}
dp@dp:~ %
編譯后,運行
dp@dp:~ % gcc -o hello hello.c
dp@dp:~ % ./hello
hello,world
使用GCC的 -S選項生成C語言對應的匯編代碼
dp@dp:~ % gcc -S hello.c
下面是剛才生成的匯編語言代碼
dp@dp:~ % cat hello.s
.file “hello.c”
.section .rodata
.LC0:
.string “hello,world”
.text
.p2align 4,15
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $4, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
addl $4, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident “GCC: (GNU) 4.2.1 20070831 patched [FreeBSD]”
.section .note.GNU-stack,"",@progbits
dp@dp:~ %
分析通過GCC編譯C語言程序生成的匯編代碼,能清楚得了解C語句運行機制、內存分配機制等隱藏在C語言代碼下的內部工作原理。下面將對helloworld程序生成的匯編進行分析。
(1)寄存器基礎知識
寄存器是中央處理器內的組成部份。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據和地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計數器。在中央處理器的算術及邏輯部件中,包含的寄存器有累加器。
雖然計算機都擁有內存,但由于CPU的運行速度一般比主內存的讀取速度快,訪問內存所需要的時間為數個時鐘周期,要訪問內存的話,就必須等待數個CPU周期從而造成浪費,因此內存并不是數據存取最快的裝置,后來在現代計算機上使用的AMD或Intel微處理器在芯片內部集成了大小不等的數據高速緩存和指令高速緩存,統稱為cache(高速緩存),cache讓數據訪問的速度適應CPU的處理速度,其原理是內存中程序執行與數據訪問的局域性行為,即一定程序執行時間和空間內,被訪問的代碼集中于一部分,但是這些仍不是訪問數據最快的途徑。
寄存器是存儲器層次結構中的最頂端,也是系統操作數據的最快速途徑,但它數量少能存儲的空間有限,它直接安放在中央處理器內,是有限存貯容量的高速存貯部件,可用來暫存指令、數據和地址。
IA-32處理器有8個通用寄存器,分別為:
EAX 一般用作累加器
EBX 一般用作基址寄存器(Base)
ECX 一般用來計數(Count)
EDX 一般用來存放數據(Data)
EBP 一般用作堆棧指針(Stack Pointer)
EBP 一般用作基址指針(Base Pointer)
ESI 一般用作源變址(Source Index)
EDI 一般用作目標變址(Destinatin Index)
IA-32處理器有6個常用的段寄存器,分別為 :
CS 代碼段寄存器
DS 數據段寄存器
SS 堆棧段寄存器
ES、FS及GS 附加數據段寄存器
它還有標志寄存器EFLAGS,用來存放有關處理器的控制標志,此外還有控制寄存器.還擁有調試寄存器和測試寄存器以及系統地址寄存器。
這些寄存器,使用的最多的是通用寄存器,在AT&T匯編中,使用%寄存器名的方式表示通用寄存器,比如:
%ebx表示ebx寄存器
%ecx表示ecx寄存器
(2)C變量內存分配
在C語言中,變量在內存中擁有自己的位置,這個位置就是變量的地址,可用指針來保存這個地址。而匯編語言中變量包括標記、數據類型、默認值三個部分,標記指示了變量的內存位置,存儲的數據類型決定了變量在內存占有多少字節的空間,默認值決定了變量的初始值。觀察上面C語言版的helloworld生成的匯編代碼中的一段(如下所示),輸出的helloworld字符串被放置在由“.LC0”標記的內存中,類型為string型,默認值為"hello,world"。
.LC0:.string “hello,world”
“.LC0”標記的內存位于應用程序的靜態分配區域,這個區域在程序運行后即被分配,即“hello,world”作為一個C語言字符串常量被安排在靜態分配區域。
還有一個非常重要內存分配區域就是堆棧,,堆棧是特殊的內存區域,用于程序中函數傳遞參數、數據的臨時存取,通常是應用程序內存范圍的結尾位置的內存區域,為了方便堆棧數據的存取,有一個堆棧指針(棧頂指針)指向堆棧中的下個內存位置,這意味著如何僅依靠棧頂指針不采用任意偏移地址機制(偏移地址可以以基地址為中心進行調整,比如說訪問某個變量,該變量的基地址為0x400,偏移地址為0x16,則該變量的最終地址為0x416),則只能按照先進后出的順序來訪問堆棧內部存儲的變量。
在匯編語言中將數據放入堆棧中,使用pushl助記符,而將數據從堆棧中彈出,使用popl助記符,每次對堆棧數據的放入與彈出都會導致棧頂指針的變化,因為棧頂指針永遠指向堆棧中下一個可用的地址。下面這段匯編完成了將10壓入堆棧,然后將10彈出到ebx寄存器中的過程。
pushl $10
popl %ebx
(2)C程序執行
C語言的源代碼被翻譯成若干行匯編代碼,由幾個簡單的指令組成的匯編代碼生成二進制文件,執行這個二進制文件,完成了helloworld的執行。
匯編語言中用的較多助記符是movl、addl、subl
movl完成數據的復制,而addl完成數據的加法,subl完成數據的減法。
這3個助記符的語法格式是:
助記符 源數據 目標數據
比如,對于這段靜態分配變量的匯編代碼:
myvalue:
.long 190mess:
.ascii “hello”通過addl與movl可以完成將myvalue指示的long類型變量190加100,然后減20的功能。
movl myvalue,%ebx
addl $100,%ebx
subl $20,%ebx
movl %ebx,myvalue
匯編語言的代碼放在了.text段中,分析上面的helloworld的反匯編代碼中一段:
.text
.p2align 4,15
.globl main
.type main, @function
globl 命令指定了main函數為入口函數(程序啟動時執行的函數),然后接著在后面定義了main函數的組成:
main:leal 4(%esp), %ecxandl $-16, %esppushl -4(%ecx)pushl %ebpmovl %esp, %ebppushl %ecxsubl $4, %espmovl $.LC0, (%esp)call putsmovl $0, %eaxaddl $4, %esppopl %ecxpopl %ebpleal -4(%ecx), %espret.size main, .-main.ident "GCC: (GNU) 4.2.1 20070831 patched [FreeBSD]".section .note.GNU-stack,"",@progbits觀察這些匯編代碼,里面充斥著pushl、popl、movl、subl與addl等助記符,C程序最終就是通過復制、入棧、出棧、加法、減法等簡單操作來完成執行的。注意觀察這些代碼中的如下幾行:leal 4(%esp), %ecxandl $-16, %esppushl -4(%ecx)pushl %ebpmovl %esp, %ebppushl %ecxsubl $4, %espmovl $.LC0, (%esp)call putsC語句的print(“helloworld”)輸出字符串就是通過上述幾行實現的,除開最后一行call puts(call指令完成調用C語言的puts函數輸出字符串的功能,puts函數向終端輸出一個字符串,其唯一的參數是char *str,str表示需要輸出的字符串)外,其它行做的所有工作就是將調用puts函數的唯一參數(指向字符串”helloworld”地址的標示“.LC0”)的放入堆棧中,以供puts函數調用,倒數第二行將.LC0標記的地址復制到當前堆棧的棧頂,前面幾行分配堆棧,調整棧頂指針,將需要保存的寄存器入棧(因為調用puts函數會破壞現有寄存器的值,稱之為保存現場),當puts函數完成后,會將入棧的寄存器值彈回各自的寄存器中(稱之為恢復現場)。
總結
以上是生活随笔為你收集整理的C指针原理(43)-helloworld的C程序汇编剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php 的不等于符号,mysql 不等于
- 下一篇: restify mysql_[菜鸟试水]