天天看點

c++ 寫x64彙編 5參數_彙編實作的memcpy和memset

奇技指南

今天為大家分享一篇關于Go優化的文章,文章中會介紹一些技巧,通過這些技巧,我們可以事半功倍的提升程式性能。這些技巧隻需要我們對程式稍加調整,不需要大的改動。希望能對大家有所幫助。

1

前言

通過這篇文章,您可以了解以下内容:

  • CPU寄存器的一些知識;
  • 函數調用的過程;
  • 彙編的一些知識;
  • glibc 中 memcpy和memset的使用;
  • 彙編中memcpy和memset是如何實作的;

閑話不多說,今天來看看彙編中如何實作memcpy和memset(腦子裡快速回憶下你最後一次接觸彙編是什麼時候......)

2

函數是如何被調用的

棧的簡單介紹

  • 棧對函數調用來說特别重要,它其實就是程序虛拟位址空間中的一部分,當然每個線程可以設定單獨的調用棧(可以使用者指定,也可以系統自動配置設定); 棧由棧基址(%ebp)和棧頂指針(%esp)組成,這兩個元素組成一個棧幀,棧一般由高位址向低位址增長,将資料壓棧時%esp減小,反之增大
  • 調用一個新函數時,會産生一個新的棧幀,即将老的%ebp壓棧,然後将%ebp設定成跟目前的%esp一樣的值即可。函數傳回後,之前壓棧的資料依然出棧,這樣最終之前進棧的%ebp也會出棧,即調用函數之前的棧幀被恢複了,也正是這種機制支撐了函數的多層嵌套調用

不管是寫Windows程式還是Linux程式,也不管是用什麼語言來寫程式,我們經常會把某個獨立的功能抽出來封裝成一個函數,然後在需要的地方調用即可。看似簡單的用法,那它背後是如何實作的呢?一般分為四步:

  • 傳遞參數,通常我們使用棧來傳遞參數,先将所有參數都壓棧處理;
  • 儲存所調用的函數的下面一條指令的位址,就是我們執行完要調用的函數,拿到結果後程式接着從哪裡繼續運作的位置,通常我們也将其壓入棧裡儲存;
  • 跳轉到被調用的函數,進行前面所說的棧幀的切換,然後執行函數主體;
  • 函數傳回,清理目前棧,之前壓棧的元素都退棧,最終恢複到老的棧幀,傳回在第二步儲存的指令位址,繼續程式的運作。

函數調用規則

函數一般都會有多個參數,我們根據函數調用時,

  • 參數壓棧的方向(參數從左到右入棧,還是從右到左入棧);
  • 函數調用完是函數調用者負責将之前入棧的參數退棧,還是被調用函數本身來作等;

這兩點(其實還有一點,就是代碼被編譯後,生成新函數名的規則,跟我們這裡介紹的關系不大)來分類函數的調用方式:

  • stdcall: 函數參數由右向左入棧, 函數調用結束後由被調用函數清除棧内資料;
  • cdecl: 函數參數由右向左入棧, 函數調用結束後由函數調用者清除棧内資料;
  • fastcall: 從左開始不大于4位元組的參數放入CPU的EAX,ECX,EDX寄存器,其餘參數從右向左入棧, 函數調用結束後由被調用函數清除棧内資料; 這種方式最大的不同是用寄存器來存參數,是以它fast。

3

glibc中的memcpy

我們先來看下glibc中的memcpy , 原型如下:

從src拷貝連續的n個位元組資料到dest中, 不會有任何的記憶體越界檢查。

大家有興趣的話可以考慮下上面的代碼輸出是什麼?

4

彙編實作的memcpy

說來慚愧,彙編代碼作者本人也不會寫。不過我們可以參考linux源碼裡面的實作,這相對還是比較權威的吧。

它的實作位于 arch/x86/boot/copy.S, 檔案開頭有這麼一行注釋Copyright (C) 1991, 1992 Linus Torvalds, 看起來應該是大神親手寫下的。我們來看一看:

  • CPU的衆多通用寄存器有%esi和%edi, 它們一個是源址寄存器,一個是目的寄存器,常被用來作串操作,我們的這個memcpy最終就是将%esi指向的内容拷貝到%edi中,因為這種代碼在linux源碼中是被辨別成了.code16, 所有這裡都隻用到這兩個寄存器的低16位:%si和%di;
  • 代碼的第一,二句儲存目前的%si和%di到棧中;
  • 這段代碼實際上是fastcall調用方式,void *memcpy(void *dest, const void *src, size_t n);
  • 其中 dest 被放在了%ax寄存器,src被放在了%dx, n被放在了%cx;
  • movw %ax, %di, 将dest放入%di中,movw %dx, %s,将stc放入%si中;
  • 一個位元組一個位元組的拷貝太慢了,我們四個位元組四個位元組的來,shrw $2, %cx,看看參數n裡面有幾個4, 我們就需要循環拷貝幾次,循環的次數存在%cx中,因為後面還要用到這個%cx, 是以計算之前先将其壓棧儲存 pushw %cx;
  • rep; movsl, rep重複執行movsl這個操作,每執行一次%cx的内容就減一,直到為0。movsl每次從%si中拷貝4個位元組到%di中。這其實就相當于一個for循環copy;
  • 參數n不一定能被4整除,剩下的餘數,我們隻能一個位元組一個位元組的copy了。
  • rep; movsb一個位元組一個位元組的copy剩下的内容;

5

glibc中的memset

我們先來看下glibc中的memset, 原型如下:

這個函數的作用是用第二個參數的最低位一個位元組來填充s位址開始的n個位元組,盡管第二個參數是個int, 但是填充時隻會用到它最低位的一個位元組。

你可以試一下下面代碼的輸出:

6

彙編實作的memset

我們還是來看一下arch/x86/boot/copy.S中的實作:

  • 不同于memcpy,這裡不需要%si源址寄存器,隻需要目的寄存器,是以我們先将其壓棧儲存 pushw %di;
  • 參考void *memset(void *s, int c, size_t n)可知,參數s被放在了%ax寄存器;參數n被放在了%cx寄存器;參數c被放在了%dl寄存器,這裡隻用到了%edx寄存器的最低一個位元組,是以對于c這個參數不管你是幾個位元組,其實多隻有最低一個位元組被用到;
  • 和memcpy一樣,一次一個位元組的操作太慢了,一次四個位元組吧,假設參數c的最低一個位元組是0x11, 那麼一次set四個位元組的話,就是0x11111111:

imull $0x01010101,%eax這句話就是把0x11變成0x11111111。

  • rep; stosl,rep重複執行stosl 這個操作,每執行一次%cx的内容就減一,直到為0。stosl每次從%eax中拷貝4個位元組到%di中。這其實就相當于一個for循環copy;
  • 參數n不一定能被4整除,剩下的餘數,我們隻能一個位元組一個位元組的copy了。

    andw $3, %cx 就是對%cx取餘,看還剩下多少位元組沒copy;

  • rep; stosl 一個位元組一個位元組的copy剩下的内容;

好了,到這裡這次的内容就結束了,有疏漏之處,歡迎指正。

關注我們

界世的你當不

隻做你的肩膀

c++ 寫x64彙編 5參數_彙編實作的memcpy和memset
c++ 寫x64彙編 5參數_彙編實作的memcpy和memset

 360官方技術公衆号 

技術幹貨|一手資訊|精彩活動

空·

繼續閱讀