歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 從匯編來看C語言之變量

從匯編來看C語言之變量

日期:2017/3/1 9:07:22   编辑:Linux編程

1、基礎研究

對如圖程序進行編譯連接,再用debug加載。

我們在偏移地址1fa處查看main函數的內容:

執行到1fd處,發現n的偏移地址為01a6,段地址存儲在ds寄存器裡,為07c4.

再查看函數f2:

參數a、b的值是用棧來傳遞的,它們的段地址都存放在ss寄存器中:

局部變量c的值在這裡是用si寄存器存儲的,因為c正好是int型,那麼子函數裡定義的局部變量是用寄存器存儲嗎?我們在這裡加一條賦值語句看看會如何:

可見,局部變量d是放在棧裡的,而c是放在寄存器si裡的,只是函數要將c返回,就將c的值賦給了ax。那麼如果返回值不是int型怎麼辦?這個問題我們之前已經研究過:如果是1字節的數據,用al存放,如果是4字節的數據,高16位用dx傳遞,低16位用ax傳遞。

也就是說全局變量n的段地址在ds寄存器裡,局部變量a、b、d的段地址在ss寄存器裡,局部變量c的值存儲在寄存器si裡而不是內存裡,沒有段地址。所以全局變量n存儲在程序開始的數據段裡,而局部變量c存儲在棧段裡。參數a、b存儲在棧段裡。函數的返回值按值的大小存儲在寄存器ax和dx中。全局變量的存儲空間在程序開始就分配了,在整個程序執行完才釋放,分配和釋放的工作應該是由c0s.obj裡的函數完成的。局部變量的存儲空間在什麼時候分配呢?我們將增加局部變量d的函數f2與之前的函數f2對比,發現多了一條語句“sub sp,2”,之後對d的賦值語句為“mov word ptr [bp-2],4”,這說明“sub sp,2”就是為局部變量d分配棧段空間的指令,局部變量是在子函數開始執行時分配的,那麼是在函數入口處將局部變量全部分配,還是在函數中局部變量定義處分配呢?因為TC2.0所使用的c標准要求在函數開頭將要使用的變量全部定義,所以在這裡這兩種方式是一樣的。而函數結束時“mov sp,bp”指令將sp的值還原,也就是釋放局部變量d的空間,所以局部變量的存儲空間是在函數結束時釋放的。從程序中可以看到,函數參數的存儲空間是在主函數裡對函數進行調用時就分配的,也就是將參數的值入棧,而在函數返回後,用pop cx將參數從棧段中釋放。

主函數裡調用f3函數使用的語句是“call 076a:0239”,也就是直接call函數的段地址+偏移地址,我們來看f3函數的內容:

發現f3返回時是用retf返回的,也就是將ip和cs都出棧。所以對於far型的函數,調用時要用call 段地址+偏移地址,返回時要用retf將段地址和偏移地址都出棧。

再來看程序2:

觀察函數f的內容:

發現n的存儲空間為si寄存器,a的存儲空間為以ds:0194為地址的兩個字節。它們的存儲空間是什麼時候分配的呢?我們知道局部變量n的存儲空間是在函數開始時分配的,而a的存儲空間是固定的內存空間,不是棧段,在函數結尾處n的空間被釋放了而a的空間並沒有被釋放。在網上查閱資料得知,靜態局部變量和全局變量分配存儲空間的方式是相同的,而且具有相同的生命周期,只是靜態局部變量只能在定義的函數中使用。

觀察主函數,也沒有釋放靜態局部變量的語句,可見靜態局部變量的存儲空間也是由c0s.obj裡的函數進行分配和釋放的。

我們觀察程序的執行結果也可以發現:

不管執行多少次f函數,每次輸出n的值都為1,因為它是局部變量,f函數結束後就要釋放,而a是靜態局部變量,相當於全局變量,它的值是可以不斷累加的。

再來看程序3:

main函數的內容為:

這裡的a、b、c、a1、a2都是全局變量,只是它們的類型不同而已。他們的存儲空間是否相鄰呢?看看偏移地址194處的數據段的內容:

可以看到數據段裡存儲了5個值為1的數,它們的存儲空間是緊鄰的。

整型的存儲空間為2個字節,字符型為1個字節,長整型為4個字節。

在自加1運算時,整型是inc word ptr,對1個字的數據進行操作;字符型是inc byte ptr,對1個字節的數據進行操作;長整型是先對低四位數據進行運算,再用位運算符adc對高四位進行運算得到結果。

下面來看程序4:

觀察main函數的內容:

我們注意對變量a、b各個數據項的賦值部分:a的每個數據項都有固定的內存地址,而b的數據項都是存儲在棧段裡面,因為a是全局變量而b是局部變量。而且a、b裡面的數據項的各自的存儲空間是相鄰的。

觀察發現,在賦值語句後,程序還有一大段的指令,這些指令是用來執行printf函數的功能的。

下面來看程序5:

main函數的內容有:

觀察發現程序中出現了lea指令,查詢可知lea指令的作用是取偏移地址。程序裡面出現了很多call指令,經過實驗,發現調用f函數的是call 0256,調用func函數的是call 0266.main函數是怎麼把結構體數據a傳給函數f的呢?我們先看看f中調用的結構體數據在什麼地方:

調用的數據在棧段裡面,a.a是bp+4,a.b是bp+6,a.c是bp+8.

那麼main函數傳值應該是將數據項壓棧的過程。

但我們發現在main函數裡從語句call 0266到call 0256之間沒有壓棧的語句,只是調用了兩個函數:call 076a:1085和call 076a:10a1,這兩個函數肯定是對結構體數據和棧進行處理的,但是我發現難以看懂看懂它們的內容。那麼不如換一種思路,我們先看看func()返回的內容放在什麼地方,下面是函數func的內容:

我們發現func在對數據項進行賦值後,同樣調用了076a:1085處的函數,而與main函數中比較,main函數是將ds、ax寄存器壓棧,而這裡是將ss、bx寄存器壓棧,即將數據項的段地址和第一項的偏移地址壓棧,再調用076a:1085進行處理。但是這個函數具體有什麼作用呢?我還無法得出結論。在網上找到下面一段話:

C 語言中函數返回結構體時如果結構體較大, 則在調用函數中產生該結構的臨時變量,並將該變量首地址傳遞給被調用函數,被調用函數返回時根據該地址修改此臨時變量的內容,之後在調用函數中再將該變量復制給用戶定義的變量,這也正是 C 語言中所謂值傳遞的工作方式。
如果結構體較小, 則函數返回時所用的臨時變量可保存在寄存器中,返回後將寄存器的值復制給用戶定義的變量即可。

我對這段話的理解是,函數076a:1085創建了一個臨時變量,將局部變量的結構體對象a的各項數據復制到這個臨時變量裡,之後函數func結束,func裡的變量a從棧中被釋放,之後main函數再調用076a:10a1,將這個臨時變量的值壓棧傳給函數f使用。

再來看看076a:10a1的內容:

觀察函數內容,發現這就是一個搬移函數,將數據項從原來的位置搬運到棧段中指定的位置,以供函數調用。076a:1085的功能也是類似的。所以從函數傳遞結構體型的數據是調用搬運函數,用movsw或movsb指令將數據項搬運到棧段中供函數調用。因為時間關系,這裡不能仔細研究,之後再繼續完善。

2、拓展研究

問題:

(1)程序1函數f2裡076a:0234處的語句jmp 0236指向的是下一條語句,這不是無意義的嗎?它起什麼作用呢?

答:這裡jmp語句是跳轉到釋放局部變量的結束語句,所以我的猜想如下:1、編譯器為了避免程序出錯所以要用jmp精確跳轉到結束語句。2、編譯器給程序預留了一個接口用來存放其他功能的程序。

這裡函數返回語句是在函數最後面,如果是選擇語句或者有多個返回語句的情況,就會出現這種情況。

(2)函數裡局部變量都是第一個定義的在si寄存器裡,其他的在棧段裡嗎?

答:不是,經過實驗,只有當該局部變量需要返回時,才存儲在si寄存器裡,否則只是存儲在棧段裡。

(3)靜態局部變量與全局變量的區別就在於在後者整個程序的所有函數裡都能訪問,而前者只能在定義的函數裡訪問嗎?

答:最明顯的區別就是作用域的區別。

(4)加載第3章的5個程序。查看偏移地址為1fa處的指令,為什麼有的程序有“push bp”和“mov bp,sp”兩條指令,有的程序沒有?

答:我的5個程序都有保護語句,如果沒有可能是編譯器的問題。

如果用TC2.0編譯,是有的,如果用tcc編譯,會出現這種情況。

(5)程序1中,全局變量n,是由“unsigned int n”這條語句定義,還是由main函數中的“n=0”這條語句定義?

答:應該是由前者定義的,函數外定義的變量,不管有沒有加static,沒有初始化的話,系統默認初始化為0。如果在n=0語句之前打印n,是能夠打印出它的值的。

(6)結構體中數據項的存儲為何不使用push、pop 指令進行操作?

答:題目的意思應該是結構體型數據參數的傳遞和返回是怎麼實現的,我們已知是通過搬運函數來實現的,即將存儲結構體函數的數據段的值整體移動到一個棧段中。那麼為什麼不通過push、pop實現呢?我覺得理由如下:1、c語言是將結構體作為一個數據類型的,和int、char等數據類型一樣,所以對它的處理方式和其他數據類型是一樣的,即要對它整體來處理,如果用push、pop的話,就要對它裡面的數據項分開來處理,這是不符合我們建立結構體數據類型的初衷的。2、如果要對它裡面的數據項分開來處理,就要知道它裡面有哪些數據項、有幾個,那麼就需要進行統計,這個是不好實現的(我還沒找到實現的方法)。3、我們只需要實現傳值的目標而不需要在這個過程中對數據進行處理,那麼就要選擇最簡單快捷、開銷最小的方法,很顯然塊搬運是最好的方式。

(7)程序4中,在聲明的局部變量struct stu b的後面,假如在後面定義一個char型變量,所占用的字節數為6(char型數據所占用字節數+局部變量struct stu b的數據項所占字節數);假如在後面定義一個整型變量,所占用字節數8,此時有了1個字節的填充,為什麼?

答:

e是int型,eee是char型,前5個變量所占空間為7個字節,加上eee才8個字節。

局部變量的情況是一樣的,加上int型的e和char型的eee也才8個字節。

如果在結構體數據後面再添加一個獨立的int型數據,會出現這種情況。內存對齊的結果,結構體內外都有可能出現。

(8)重新研究,不同類型的變量,存儲空間的分配情況。

答:char型變量占1個字節,int型的變量占2個字節,long型占4個字節。int型在TC裡占2個字節,在VC字節裡占4個字節。因為TC模擬的是16位dos操作系統,VC模擬的是32位操作系統。

(9)再次對程序5進行研究,找到每一條c語句對應的匯編代碼。

Struct n a;

Int b;--------------------sub sp,6

a=func();----------push ss;

push bx;

call 0266;

push ds;

Push ax;

mov cx,6;

Call 076a:1085;

b=f(a);------------lea bx,[bp-6]

Mov dx,ss

Mov ax,bx

Mov cx,6

Call 076a:10a1

Call 256

Printf(“%d”,b);---mov si,ax

Push si

mov ax,194;

Push ax

Call 093a

Printf(“%d”,f(func()));---call 266

Mov dx,ds

Mov cx,6

Call 076a:10a1

Call 256

Add sp,6

Push ax

Mov ax,198

Push ax

Call 93a

Func():

Struct n a;-----------sub sp,6

a.a=1;----------------mov word ptr [bp-6],1

a.b=2;----------------mov word ptr [bp-4],2

a.c=2;----------------mov word ptr [bp-2],3

Return a;-------------mov bx,426

Push ds;

Push bx;

Lea bx,[bp-6]

Push ss;

Push bx;

Mov cx,6

Call 076a:1085

(10)全局變量、局部變量存儲方式的不同有什麼普遍的意義?

答:我們把這裡的局部變量理解為動態局部變量。全局變量的存儲空間是固定的,局部變量是動態分配的,他們的存儲方式決定了他們的特點:1、作用域。全局變量在該程序所有地方都可以使用,局部變量只能在定義的函數裡使用。2、生命周期。全局變量的生命周期和整個程序是一樣的,而局部變量的生命周期與函數一樣,函數結束即釋放。這種方式更有利於減小程序的內存開銷,避免變量定義出錯,保證函數的獨立性,使程序模塊化,方便編寫和調試。

全局變量放在數據段中,局部變量放在棧段中。比如一個程序有100個函數,每個有5個局部變量,如果都放在數據段中,就會造成內存開銷太大,不好管理和調用,所以要用棧段來存放局部變量,這就是高級語言的核心機制。

3、研究總結

本章研究了函數的各種類型的變量的存儲方式,是比較重要的一章。

Copyright © Linux教程網 All Rights Reserved