1、(1)數字分兩種:整數和小數。之前介紹了整數溢出,本文介紹小數(浮點數)的存儲和表示方法;整數的表示方法很簡單:按照一定的計算方式轉成二進制即可,比如10進制的9轉成二進制1001,內存中最小存儲單元是字節,也就是8bit;如果用1byte存儲9,那么轉成二進制就是00001001,這個應該不難理解;那么小數或浮點數該怎么表示和存儲在內存了?
(2)舉個例子:6.625這個浮點數,整數部分是6,小數部分是0.625,要想存儲在內存,必須先轉成二進制的形式;整數部分和小數部分轉換的方式剛好相反,如下:
(2.1)整數部分6:這個很常見,每次除以2,用商繼續除以2,直到商為0為止。6的二進制表示就是110;
(2.2)小數部分0.625:這個該怎么表示成二進制了? 整數部分是除以2,小數部分剛好反着,就是乘以2,計算過程如下:小數部分就是101
(2.3)6.625用二進制小數“表示”就是:110.101;怎么驗證110.101轉成10進制后就是6.625了?整數部分很好驗證:2^2+2^1+0*2^0=6,小數部分怎么驗證了?形式和整數是一樣的!
1*2^(-1)+0*2^(-2)+1*2^(-3)=0.5+0+1/8 = 0.625
通過上述互相轉換驗證了小數部分轉成二進制方式是正確的!
(3)內存從硬件上看都是無數個小電容構成的。電容充電就是1,放電就是0;所以內存中所有的數都是0101這種二進制串,是沒法直接表示小數點的,這種情況只能繼續通過人為抽象存儲格式來存浮點數了。目前國際通用的浮點數存儲規則是IEEE754,浮點數存儲格式如下:
按照上面的標准,110.101 = 1.10101*2^2( 二進制小數點左移兩位,相當於翻2^2倍,和位運算中的移位是一樣的!) ,此時進一步抽象出了幾個關鍵的“描述符”:s=0,M=1.10101,E=2;到了這里上述問題還是存在:內存硬件上只能存01二進制串,怎么存(表)儲(示)小數點了?
(4)第(3)步抽象出s、M、E這些“描述符”后,利用這些信息是不是已經完全能夠還原110.101這個浮點數了?答案是肯定的!套用上述的公式,能得到1.10101*2^2,根據這個進一步還原得到110.101;
所以在內存中存儲浮點數,等價於在內存中存儲s、M、E這些關鍵信息;
(5)s、M、E這些關鍵信息又是按照什么標准或方式在內存中存放的了?格式如下,以4字節的float為例: 最高位31位放s,23~30這8bit存放E,剩下23bit存放M;
幾點需要注意:
- E是指數,所以有可能是負數,這8bit是怎么表示負指數的了?這里就和有符號數表示有本質區別了。有符號數最高位1表示負數,0表示整數,但這里不這么干,而是指數+127;比如這里的指數是2,那么E=127+2=129=1000 0001(所以這里指數最大不能超過2^127-1,表示浮點數足夠了!);如果指數是負數,比如-5,那么E=127-5=122=111 1010;
- M:所有M中小數點左邊都是1,所以這1位可以省略,直接存剩下的位數;
- M:只有23位,如果10進制的小數部分乘以2始終不為1,那么只取23位,其余的舍棄,所以這是部分浮點數有誤差、無法精確表示的根本原因!最低為舍棄時如果是1就不能直接舍棄,還要+1;
所以6.625這個浮點數經過一系列轉換后,最終在內存中的存儲形式如下:
cpu從內存中讀取這4字節數據后,按照上面的規則反過來計算出10進制的浮點數!
2、這里來看一道2018護網杯CTF題目:getting_start;題目的代碼是醬紫的:
這里需要執行system("bin/sh")這行代碼,也就是說上面if條件不能成立;這里v7已經相等了,只能看v8了;v8已經有了初始值,怎么才能讓v8=0.1了、讓if條件不成立了?
這里能接受用戶輸入的地方有read,數據保存在buf中,所以這里只能通過控制buf的值來改變棧上的數據,當然也包括v8了!老規矩,先畫個棧圖,能直觀理解:
buf距離v8有32字節,而read會讀取0x28=40字節,所以如果buf全部讀取40字節,連下面的v9都能覆蓋!不過這里v9和本題無關,暫時放過它!buf、v5、v6隨便寫,那么前24字節隨便寫,這里用“a”替代;由於v7要保持原值,所以第25~28字節是0x7FFFFFFFFFFFFFFF;接下來就是最重點的部分了:29~32字節、也就是v8怎么填寫二進制數據,才能讓這里的值等於0.1?這里先偷個懶:下面參考這里鏈接有個工具,可以直接把double類型的小數轉成二進制形式,如下:
那么最后4字節就是0x3FB999999999999A,所以最終的payload=b"a"*24+ p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A);
3、站在學習的角度,這里從頭開始手搓一下0.1在內存中的存放形式;
(1)0.1是10進制,先轉成小數二進制的形式;按照之前的方法,每個步驟如下:
這里出現了一個有趣的現象:從第6步開始出現了循環,所以沒必要再挨個計算了,這里直接寫出二進制的形式:0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011.....
(2)二進制的形式有了,現在要提取s、M、E三個要素,這里要繼續做格式轉換。小數點向右移4位才遇到第一個1,所以這時表達式變成了:
1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011*2^(-4)
這里三個要素就明確了:s=0,M=1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001 , E=(2^10-1)-4=1023-4=1019=011 1111 1011;
注意:這里的M只保留52位時,最后一位是1,這位不能直接去掉,要在末尾+1(小數存放的誤差就是這么來的),所以真正的M = 1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
(3)再按照標准把s、M、E首尾拼接(我這里為了區分三要素,人為用||隔開了,實際上是沒有||的):0 || 011 1111 1011 || 1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010 ,也就是0011 1111 1011 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 = 0x3FB999999999999A;
參考:
1、https://www.bilibili.com/video/BV1j7411H7Fc?p=8 浮點數進制轉換(講的很詳細,強烈建議看完)
2、http://www.binaryconvert.com/result_double.html?decimal=048046049 非常好用的浮點數(float或double)10進制和2進制之間轉換的工具