匯編實現的memcpy和memset的方法
天天山珍海味的吃,也會煩。偶爾來點花生,毛豆小酌一點,也別有一番風味。
天天java, golang, c++, 咱們今天來點匯編調劑一下,如何?
通過這篇文章,您可以了解過:
- CPU寄存器的一些知識;
- 函數調用的過程;
- 匯編的一些知識;
- glibc 中 memcpy和memset的使用;
- 匯編中memcpy和memset是如何實現的;
閑話不多說,今天來看看匯編中如何實現memcpy
和memset
(腦子里快回憶下你最后一次接觸匯編是什么時候......)
函數是如何被調用的
棧的簡單介紹
- 棧對函數調用來說特別重要,它其實就是進程虛擬地址空間中的一部分,當然每個線程可以設置單獨的調用棧(可以用戶指定,也可以系統(tǒng)自動分配); 棧由棧基址(%ebp)和棧頂指針(%esp)組成,這兩個元素組成一個棧幀,棧一般由高地址向低地址增長,將數據壓棧時%esp減小,反之增大;
- 調用一個新函數時,會產生一個新的棧幀,即將老的%ebp壓棧,然后將%ebp設置成跟當前的%esp一樣的值即可。函數返回后,之前壓棧的數據依然出棧,這樣最終之前進棧的%ebp也會出棧,即調用函數之前的棧幀被恢復了,也正是這種機制支撐了函數的多層嵌套調用;
不管是寫Windows程序還是Linux程序,也不管是用什么語言來寫程序,我們經常會把某個獨立的功能抽出來封裝成一個函數,然后在需要的地方調用即可??此坪唵蔚挠梅?,那它背后是如何實現的呢?一般分為四步:
棧
函數調用規(guī)則
- 函數一般都會有多個參數,我們根據函數調用時,
- 參數壓棧的方向(參數從左到右入棧,還是從右到左入棧);函數調用完是函數調用者負責將之前入棧的參數退棧,還是被調用函數本身來作等
這兩點(其實還有一點,就是代碼被編譯后,生成新函數名的規(guī)則,跟我們這里介紹的關系不大)來分類函數的調用方式:
- stdcall: 函數參數由右向左入棧, 函數調用結束后由被調用函數清除棧內數據;
- cdecl: 函數參數由右向左入棧, 函數調用結束后由函數調用者清除棧內數據;
- fastcall: 從左開始不大于4字節(jié)的參數放入CPU的EAX,ECX,EDX寄存器,其余參數從右向左入棧, 函數調用結束后由被調用函數清除棧內數據;
這種方式最大的不同是用寄存器來存參數,所有它fast。
glibc中的memcpy
我們先來看下glibc中的memcpy , 原型如下:
void *memcpy(void *dest, const void *src, size_t n);
從src拷貝連續(xù)的n個字節(jié)數據到dest中, 不會有任何的內存越界檢查。
char dest[5] = {0}; char test[5] = {0,'b'}; char src[10] = {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}; ::memcpy(dest, src, 6); std::cout << src << std::endl; std::cout << dest << std::endl; std::cout << test << std::endl;
大家有興趣的話可以考慮下上面的代碼輸出是什么?
匯編實現的memcpy
說來慚愧,匯編代碼作者本人也不會寫。不過我們可以參考linux源碼里面的實現,這相對還是比較權威的吧。
它的實現位于arch/x86/boot/copy.S
, 文件開頭有這么一行注釋Copyright (C) 1991, 1992 Linus Torvalds
, 看起來應該是大神親手寫下的。我們來看一看
GLOBAL(memcpy) pushw %si pushw %di movw %ax, %di movw %dx, %si pushw %cx shrw $2, %cx rep; movsl popw %cx andw $3, %cx rep; movsb popw %di popw %si retl ENDPROC(memcpy)
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中;
一個字節(jié)一個字節(jié)的拷貝太慢了,我們四個字節(jié)四個字節(jié)的來,shrw $2, %cx
,看看參數n里面有幾個4, 我們就需要循環(huán)拷貝幾次,循環(huán)的次數存在%cx中,因為后面還要用到這個%cx, 所以計算之前先將其壓棧保存pushw %cx
;
rep; movsl
,rep
重復執(zhí)行movsl
這個操作,每執(zhí)行一次%cx的內容就減一,直到為0。movsl每次從%si中拷貝4個字節(jié)到%di中。這其實就相當于一個for循環(huán)copy;
參數n不一定能被4整除,剩下的余數,我們只能一個字節(jié)一個字節(jié)的copy了。
andw $3, %cx
就是對%cx取余,看還剩下多少字節(jié)沒copy;
rep; movsb
一個字節(jié)一個字節(jié)的copy剩下的內容;
glibc中的memset
我們先來看下glibc中的memset, 原型如下:
void *memset(void *s, int c, size_t n);
這個函數的作用是用第二個參數的最低位一個字節(jié)來填充s地址開始的n個字節(jié),盡管第二個參數是個int, 但是填充時只會用到它最低位的一個字節(jié)。
你可以試一下下面代碼的輸出:
int c = 0x44332211; int s = 0; ::memset((void*)&s, c, sizeof(s)); std::cout << std::setbase(16) << s << std::endl; // 11111111
匯編實現的memset
我們還是來看一下arch/x86/boot/copy.S
中的實現:
GLOBAL(memset) pushw %di movw %ax, %di movzbl %dl, %eax imull $0x01010101,%eax pushw %cx shrw $2, %cx rep; stosl popw %cx andw $3, %cx rep; stosb popw %di retl ENDPROC(memset)
不同于memcpy
,這里不需要%si源址寄存器,只需要目的寄存器,所以我們先將其壓棧保存pushw %di
;
參考void *memset(void *s, int c, size_t n)
可知,參數s被放在了%ax寄存器;參數n被放在了%cx寄存器;
參數c被放在了%dl寄存器,這里只用到了%edx寄存器的最低一個字節(jié),所以對于c這個參數不管你是幾個字節(jié),其實多只有最低一個字節(jié)被用到;
和memcpy
一樣,一次一個字節(jié)的操作太慢了,一次四個字節(jié)吧,假設參數c的最低一個字節(jié)是0x11, 那么一次set四個字節(jié)的話,就是0x11111111:
movzbl %dl, %eaximull $0x01010101,%eax
imull $0x01010101,%eax
這句話就是把0x11
變成0x11111111
rep; stosl,
rep重復執(zhí)行
stosl 這個操作,每執(zhí)行一次%cx的內容就減一,直到為0。stosl每次從%eax中拷貝4個字節(jié)到%di中。這其實就相當于一個for循環(huán)copy;
參數n不一定能被4整除,剩下的余數,我們只能一個字節(jié)一個字節(jié)的copy了。
andw $3, %cx
就是對%cx取余,看還剩下多少字節(jié)沒copy;
rep; stosl 一個字節(jié)一個字節(jié)的copy剩下的內容;
總結
以上所述是小編給大家介紹的匯編實現的memcpy和memset的方法,希望對大家有幫助!
相關文章
匯編語言DOSBox及debug.exe在Windows64下環(huán)境搭建
這篇文章主要為大家介紹了匯編語言環(huán)境的搭建DOSBox及debug.exe在Windows64下安裝配置過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11