1.信息表示

常用数字编码

原码

就是最简单的,使用一位作为符号位,0就是负数1是正数

补码

正数的补码就是原码、

负数的补码就是取反加一(除了符号位),也就是反码加一

移码

就是所有的数据都加上一个bias(偏移量)就可以把所有的数据转化为正数,比如-128~127的数加上128等等,具体见使用的时候的选择(在表示浮点数的阶码里有使用到)

反码

就是对于负数除了符号位之外全部直接进行取反的操作

为什么采用这些编码

对于正数而言,三种编码的数值是一样的,补码可以保证加减运算的统一性

x:
1 1 1 0 1 1 0 0
y:
0 0 0 0 0 0 1 0

注意这里的x,如果x是个有符号数字,虽然我们给的是一个ec正数,但是由于截断处理,最高位为1,成了一个负数,又由于这是补码保存,原码就成了-20,y仍然为2
当作无符号数看时,第一位的符号位不看,x = 236 ,y = 2 。
  但是从计算机的角度看,这就是一串二进制,哪有什么有符号无符号数字
  下面用 add 指令进行加法运算,计算机开始工作,它只需要把每一位相加,仅此而已,它才不分什么有符号,无符号。
结果:
1 1 1 0 1 1 1 0

这个结果当作有符号数就是:-18 ,无符号数就是 238 。同样,计算机认为这仍然是一串二进制,所以add 一个指令可以适用有符号和无符号两种情况。(呵呵,其实为什么要补码啊,就是为了这个呗,:-))

浮点数编码

32位浮点数的第一位是符号位

第二到第九是阶码(阶码=移码-1,就是使用移码表示的几次方(一共是8位数字,也就是需要减去128(但是在新的标准里会使用127作为偏移量,11111110—>254-127=127

然后就是最后的尾数了,一共是23位因为科学计数法的原因,第一位一定是1就会直接省略了,所以可以表示24位的尾数

在反向转化的时候就先拿出符号位,然后接下来的八位转化为十进制,然后减掉127就会得到阶码也就是2的几次幂,最后的数字串就在前面补上1然后小数点在补上的1后面,就还原为原来的数字了

浮点数也是分单精度和双精度的,单精度的1位符号位,8位指数,23位小数,双精度的是1位符号位,11位指数,52位小数(float32位,double64位)

数据的存储

有小端模式和大端模式

假如说现在有一段数据AB CD EF GH 12 34 56 78(地址大小从左到右递增)

如果作为int 型的数据进行解释,首先可以知道这是一段16进制的数据,那么一个字符就是四个0/1序列,一个存储单元(8个0/1序列,作为一个整体)就是两个字符,那么一个int(32位)的数据就是对应着8个字母,小端方式的解释就是GHEFCDAB来翻译为十进制,后面的就是78 56 34 21。而大端的方式就比较像人的思考方式,就是直接从左到右读取就可以了

总结一下就是大端的存储方式就是最高有效字节在最低的地址,小端的方式就是最高有效字节放在了最高的地址里

2.整数的运算

1.按位计算

按位与

按位或

按位异或

按位取反

移位运算

作为二进制数串,往往左移和右移就意味着除法和乘法,而这种移位的运算在计算机的底层里计算的很快

无符号数

直接左移或者是右移,然后在空位直接进行补0

有符号数

逻辑右移

就是补0

算术右移

就是补上符号位

左移

就是和无符号数是一样的

类型转化

拓展

无符号数拓展就直接补0,有符号数是符号位拓展,就是补符号位的值

截断

直接就把高位的值就不要了

2. 数字计算

加法

在机器的眼里是没有带符号和无符号数的,他会做的事情就是把所有的数转化为补码进行计算(正数没有变化,换句话说就是把负数取反加一)然后进行加法就行。

条件码引入

零标志就是ZF,溢出OF,借位/进位标志CF,符号标志SF

OF:计算A,B同符号但是和sum不同符号就是1(不同符号的数相加不会有溢出的可能)

SF:就是sum的符号,描述的符号位的信息,显然也是针对带符号数的

ZF:如果sum是0就是1

CF:就是cin和cout的异或(这里的cin不是数字与电路里的下一个加法器的进位,而是控制计算加法还是减法的数字)(这个地方没有看懂)(解释:对于计算机而言,他也是转化为补码进行计算,就是负数(减数)全部取反,然后就是:X+Y(取反)+1=结果。举个简单的例子就是5-8=0101-1000=0101+0111+1=1101,没有发生进位,cout=1,cin=1,那么就是CF=1。其实就是原来的减数越大,那么取反之后的值就是越小的,那么相加的时候就不会有溢出,反而是说明了原本的减法计算是发生了借位的)cin=1的时候做的是减法,CF=1表示有进位或者是借位

PS:CF对于无符号数而言是溢出的标志,OF对无符号数没有意义。OF是对有符号数溢出标志

1111+0111=0110进位1,对于带符号数而言都对,但是对于无符号数而言是一种溢出,0111+0001=1000对于无符号数而言一点问题都没有,而对于有符号数而言是一种溢出

参考链接:https://blog.csdn.net/qq_33448537/article/details/62218470

减法

减法其实也就是加法里的一个数是负数

通过减法以及计算得到的条件码可以进行比较大小的操作

乘法

乘法本质是多次的加法,比较耗费时间,一般是使用移位加上加法来表示(比如说乘上20就是乘上16(移位四次)加上乘上4(移位两次)的结果)

乘法就是n位乘上n位最后会得到2*n位的结果,但是处理器会只使用最后的n位保留其他的截断

带符号和无符号的乘法器是不一样的,不一样在于被舍弃的高n位,也就是说如果不需要高n位的数据的话(做一些溢出或者是条件码的判断的)就可以使用无符号的计算有符号的

乘法判断溢出分为机器判断和高级语言程序员进行判断,对于机器而言也有分有符号和无符号的,对于有符号的判断就是高n位的值和低n位的最高位是一样的(也就是从高位数下n+1位下来的值都是一样的),对于无符号的判断就是高n位都是0(就是没有进位溢出),对于程序员来说判断的方法就是(x==0||a/x=y)逆向计算进行检查

溢出可能会有引发缓冲区攻击下面是例子

  1. int copy_array(int *array, int count)
  2. {
  3. int i;
  4. /*开始申请一段堆区的空间*/
  5. int *myarray=(int *)malloc(count*sizeof(int));
  6. if(myarray=NULL)
  7. return -1;
  8. for(i=0;i<count;i++)
  9. {
  10. myarray[i]=array[i];
  11. }
  12. return count;
  13. }
  14. /*这段代码的问题就在于当count的值很大的时候就有可能在计算count*sizeof(int)的时候发生溢出,溢出导致值的截断,也就是乘法的结果会比原来的数count都要小,就会导致申请的空间其实很少,但是程序不知道,然后就会覆盖大量堆区的数据造成数据的损失
  15. */

除法

汇编语言 - 图165536%2B%5Brem(H%2FN)65536%2BL%5D%2FN#card=math&code=X%2FN%3Dint%28H%2FN%29%2A65536%2B%5Brem%28H%2FN%29%2A65536%2BL%5D%2FN)

溢出问题

只有汇编语言 - 图2会有发生溢出的情况

详情可以看这里:https://blog.csdn.net/lingang_/article/details/2138105

精度问题

整数除法的结果也是整数,朝零的方向进行舍入。

除法花的时间很多,一般就是能用右移来代替就是用右移来代替

如果可以整除(那么右移的位里面一定就都是零),如果不能整除,那么移除的位里面一定是有不是零的数。

那么对于无符号数而言就是直接删除移出位,对于带符号的负整数而言是先加上汇编语言 - 图3(k是右移的位数)然后在右移,最后在截断(因为如果是负数的话,舍入导致补码变大,也就是商的值变小了,和向0舍入的思路不一致,所以引入了偏移量)

常见问题

上述的整数运算里一个经常出现的问题就是数据类型转化,尤其是有无符号数之间的转化,尤其是C语言里有的函数得到的整数就是一个无符号数比如strlen。然后在有无符号数直接的计算的时候就会出现意想不到的类型转化导致结果出现问题,C语言的类型转化不改变内存的数据,只会改变对于数据的解释方式

3.浮点数计算

整数除于0会引发异常

但是浮点数除于0不会,正整数除于零就是正无穷大,负的就是负无穷大

浮点数的加减运算

把次数小的变成次数大的(对阶)(这里注意阶码的上溢和下溢),然后进行正常的加减运算,然后进行规格化(规定的小数点前一定会有一位的,那么就是要改变阶码移动小数点,同样注意上溢和下溢)上溢就是结果溢出,下溢就是0(非常小)

浮点数的舍入

附加位的引入

规定位的右边一位是保护位,再右边一位是舍入位,取两位的附加位

舍入方式

就近舍入

查看附加位,如果是更接近左边最近的一个数,那就舍去,如果是接近最右边的数那就入,如果刚刚好在中间那么就强制舍入的结果的是一个偶数

正无穷大舍入

就是正向舍入位右边的数

负无穷大舍入

就是负向舍入为左边的数

向0舍入

正数就取左边,负数就取右边,说白了就是向着0的方向

(说明:这里的左右的概念是基于数轴的,右边是正方向)

  1. int main()
  2. {
  3. float a;
  4. double b;
  5. a=123456.789e4;
  6. b=123456.789e4;
  7. printf("%f/n%f/n",a,b);
  8. }

4.指令系统

指令系统概述

机器级指令和汇编指令是一一对应的。机器级指令就是0/1序列,汇编指令是机器指令的符号化形式,本质是一样的。汇编指令可以有不同的形式比如intel形式和AT&T

生成汇编代码的指令

-E生成预处理结果,-S生成汇编语言,可以先预处理再生成汇编也可以直接生成

-C生成可重定位的目标代码(二进制0/1序列)需要通过反汇编进行查看

编译得到的汇编和反汇编的得到的汇编代码有一定的差异

可执行的目标文件是没有后缀的

IA-32指令系统就是规定了指令的规则标准,变成通用计算机

寻址方式

简单的说明:

建议是看后面的附录里面的更加的详细

操作数的位置:指令中,寄存器中,存储单元中

对应的寻址方式:立即寻址,寄存器寻址,其他寻址方式

在指令中直接给出操作数 —> 立即寻址

在指令中给出操作数所在的寄存器的编号 —>寄存器寻址

比较复杂的(SR:[B]+[I]*s+A),SR是段基址,后面的是有效地址(偏移量)B是基址寄存器,I是变址寄存器,S是比例因子,A是位移量,这种方式得到的操作数是在存储器里的,这种的寻址方式是比较复杂的(例如:8(%edx,%eax,4)这里的4是因子,8是位移量,%edx是B,%eax是I)

指令执行的时候的跳转是用到的相对寻址

5.C语言下的机器级代码

1. 函数调用过程中的机器代码

函数调用过程种的汇编代码

栈帧结构

ebp指向的当前栈帧的底部(高地址),esp指向的是当前栈帧的顶部(高地址),栈区是从高地址向低地址生长的。

整体的过程分析

  1. 准备阶段:形成栈帧结构,push和mov指令。保存当前栈帧的栈顶位置,然后通过sub和and指令进行移动栈顶的位置生成栈帧,随后保存现场(如果有被调用者保存寄存器(部分数据可能会被调用进程使用,所以需要保存)),使用mov指令
  2. 过程函数体,分配局部变量空间并赋值(就是在形成的新的栈帧结构里使用mov指令,然后进行处理逻辑(如果是有新的函数调用的话,就准备参数,送到帧入口处,然后使用call指令,保存返回地址并转到被调用函数)。最后在eax里准备返回值
  3. 最后是结束阶段,使用leave指令或者是pop指令及逆行退栈,取到返回地址之后返回:ret指令

2.选择和条件语句中的机器级代码

其实是和数据与计算里所了解的差不多就是,通过的就是一些条件判断和跳转指令来实现代码功能

值得一说的就是switch语句,是对判断依据的进行取值范围的判断,然后进行相应的数据比较决定执行哪一个语句

举个简单的例子:switch的选择标准a有11,13,15,那么用a-11作为条件判断,在a-11>4或者是a-11<0的时候都会执行默认的语句(a-11<0,根据无符号数的解释会解释为一个很大的数,那么就是和a-11>4归为一类了,然后a-11的结果分别去和那些对应的数据进行比较和跳转。在对比区间里没有数据比如12,14就设置为跳转去执行默认语句

6.复杂数据类型的汇编表示

结构体

保存首地址,然后使用偏移量的方法对每一个成员进行访问比较占据空间,尤其是考虑对齐的时候,在结构体作为函数的参数的时候经常是使用传递地址,而不是传递数据(具体原因略)

联合体

减小空间的消耗,但是有可能造成处理复杂度的提升

  1. unsigned hhh(float f)
  2. {
  3. union{
  4. float f;
  5. unsigned u;
  6. }temp;
  7. temp.f=f;
  8. return temp.u
  9. }
  10. /*这个操作不是在强制类型转化而是一个对于相等的数据进行的不同的解释,因为union的数据结构是共享一个数据内存空间的,所以值没有改变,知识使用不同的方法进行读取*/

可以看出机器级的代码只处理0/1序列,在他的眼里就没有各种的数据类型

数据对齐

机器在阅读数据的时候经常按照传送单位为基本单位来做(32位或者是64位)数据的存储分为边界对齐和边界不对齐(一个加速一个省空间)

结构体里变量声明的顺序会影响到速度和空间占用

  1. struct S1{
  2. int i;
  3. int j;
  4. char k;
  5. }
  6. struct S2{
  7. int i;
  8. char k;
  9. int j;
  10. }
  11. /*显然是S1的定义顺序会更好*/

在高级语言中指定数据对齐的方式

pragma pack(n): 直接为编译器指定结构体或者是类内部成员变量的对齐方式,当自然边界大于n的时候使用n对齐,n缺省的时候直接使用自然边界进行对齐

attribute((aligened(m)))为编译器指定一个结构体或类或联合体或一个单独变量的对齐方式,直接按照m字节进行对齐(前提m是2的n次幂,占用空间大小也是m的整数倍,保证连续申请空间的时候仍然可以保证按照m字节进行对齐,比如说m指定为1024,那么这个结构体变量的起始地址一定是1024的倍数,所有变量所占的空间也是1024的倍数,可能会有出现空余的位置(个人认为计算这样的数据结构所占据的大小就是使用自然对齐的结果向最近的倍数靠齐,比如15->16)

attribute((packed))就是指定不按边界进行对齐

7.缓冲区溢出和越界访问

通常就是在数组访问越界导致修改了不该修改的数据

  1. #include"stdio.h"
  2. #include"string.h"
  3. void outputs(char *str)
  4. {
  5. char buffer[16];
  6. strcpy(buffer,str);
  7. printf("%s\n",buffer);
  8. }
  9. void hacker(void)
  10. {
  11. printf("being hacked\n");
  12. }
  13. int main(int argc,char *argv[])
  14. {
  15. outputs(argv[1]);
  16. return 0;
  17. }
  18. /*这里就是利用了缓冲区溢出实现的攻击,在strcpy的地方可能会冲掉机器代码的返回地址处,使得程序转到黑客所需的程序位置*/

攻击代码

  1. #include"stdio.h"
  2. char code[]="shdajhsd\x11\x78\x08\x00";
  3. int main(void)
  4. {
  5. char *argv[3];
  6. argv[0]="./test";//可执行程序的文件名
  7. argv[1]=code;//命令行执行参数
  8. argv[2]=NULL;
  9. execve(argv[0],argv,NULL);//这个系统封装的程序调用函数,可以根据参数来调用程序
  10. return 0;
  11. }

8.附录

各种数据类型的大小

数据类型 32位 64位 取值范围(32位)
char 1 1 -128~127
unsigned char(当byte使用) 1 1 0~255
short int /short 2 2 –32768~32767
unsigned short 2 2 0~65535
int 4 4 -2147483648~2147483647
unsigned int 4 4 0~4294967295
long int /long 4 8 –2147483,648~2,147483,647
unsigned long 4 8 0~4,294,967,295
long long int/long long 8 8 -9223372036854775808~9223372036,854775807
指针 4 8
float 4 4 3.4E +/- 38 (7 digits)
long double 80/96位
double 8 8 1.7E +/- 308 (15 digits)

汇编过程

生成可执行文件gcc test.c -o test

分步走就是

  1. 预处理gcc -E test.c -o test.i
  2. 编译为汇编代码gcc -S test.i -o test.s
  3. 汇编,编译为目标文件gcc -c test.s -o test.o
  4. 链接,链接连接库gcc test.o -o test

(详细内容的网址https://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html)

简单的反汇编指令

objdump指令

  • -d:将代码段反汇编
  • -S:将代码段反汇编的同时,将反汇编代码和源代码交替显示,编译时需要给出-g,即需要调试信息。
  • -C:将C++符号名逆向解析。
  • -l:反汇编代码中插入源代码的文件名和行号。
  • -j section:仅反汇编指定的section。可以有多个-j参数来选择多个section。
  • 更多的详细内容见这里https://blog.csdn.net/zoomdy/article/details/50563680

简单的概念区别

算术左移和逻辑左移是一样的,但是逻辑右移和算术右移是不一样的,逻辑右移直接加上0,算术右移是加上符号位

Linux常用指令

  1. 创建文件 touch sth

  2. 创建目录 mkdir sth

  3. 浏览编辑文件 vim sth

  4. 删除文件夹 rm -rf sth

  5. 汇编和编译的指令见上文

浏览汇编代码中问题

cfi_def_cfa cfi_endproc cfi_startproc的命令,这些前面都有个关键字cfi 它是Call Frame infromation的意思,查看堆栈的信息。详细信息看这里:https://www.cnblogs.com/justinyo/archive/2013/03/08/2950718.html

AT&T汇编指令补充

https://blog.csdn.net/dayancn/article/details/51190424

后缀:

C声明 GAS后缀 大小(字节)
char b 1
short w 2
(unsigned) int / long / char* l 4
float s 4
double l 8
long double t 10/12

l表示4字节的整数和8字节的双精度浮点数,没有歧义因为浮点数使用的完全不同的指令和寄存器

格式 操作数值 名称 样例(GAS = C语言)
$Imm Imm 立即数寻址 $1 = 1
Ea R[Ea] 寄存器寻址 %*eax = eax
Imm M[Imm] 绝对寻址 0x104 = *0x104
Ea M[R[Ea]] 间接寻址 %eax= *eax
Imm(Ea) M[Imm+R[Ea]] (基址+偏移量)寻址 4(%eax) = *(4+eax)
Ea,Eb M[R[Ea]+R[Eb]] 变址 (%eax,%ebx) = *(eax+ebx)
ImmEa,Eb M[Imm+R[Ea]+R[Eb]] 寻址 9(%eax,%ebx)= *(9+eax+ebx)
(,Ea,s) M[R[Ea]*s] 伸缩化变址寻址 (,%eax,4)= (eax4)
Imm(,Ea,s) M[Imm+R[Ea]*s] 伸缩化变址寻址 0xfc(,%eax,4)= (0xfc+eax4)
(Ea,Eb,s) M(R[Ea]+R[Eb]*s) 伸缩化变址寻址 (%eax,%ebx,4) = (eax+ebx4)
Imm(Ea,Eb,s) M(Imm+R[Ea]+R[Eb]*s) 伸缩化变址寻址 8(%eax,%ebx,4) = (8+eax+ebx4)

注:M[xx]表示在存储器中xx地址的值,R[xx]表示寄存器xx的值,这种表示方法将寄存器、内存都看出一个大数组的形式。

数据传送指令:

指令 效果 描述
movl S,D D <— S 传双字
movw S,D D <— S 传字
movb S,D D <— S 传字节
movsbl S,D D <— 符号扩展S 符号位填充(字节->双字)
movzbl S,D D <— 零扩展S 零填充(字节->双字)
pushl S R[%esp] <— R[%esp] – 4;*M[R[%esp]] <— S 压栈
popl D D <— M[R[%esp]]R[%esp] <— R[%esp] + 4; 出栈

注:均假设栈往低地址扩展。

算数和逻辑操作地址:

指令 效果 描述
leal S,D D = &S movl地址,S地址入DD仅能是寄存器
incl D D++ 1
decl D D— 1
negl D D = -D 取负
notl D D = ~D 取反
addl S,D D = D + S
subl S,D D = D – S
imull S,D D = D*S
xorl S,D D = D ^ S 异或
orl S,D D = D | S
andl S,D D = D & S
sall k,D D = D << k 左移
shll k,D D = D << k 左移(sall)
sarl k,D D = D >> k 算数右移
shrl k,D D = D >> k 逻辑右移

特殊算术操作:

指令 效果 描述
imull S R[%edx]:R[%eax] = S * R[%eax] 无符号64位乘
mull S R[%edx]:R[%eax] = S * R[%eax] 有符号64位乘
cltd S R[%edx]:R[%eax] = 符号位扩展R[%eax] 转换为4字节
idivl S R[%edx] = R[%edx]:R[%eax] % S;*R[%eax] = R[%edx]:R[%eax] / S; 有符号除法,保存余数和商
divl S R[%edx] = R[%edx]:R[%eax] % S;*R[%eax] = R[%edx]:R[%eax] / S; 无符号除法,保存余数和商

注:64位数通常存储为,高32位放在edx,低32位放在eax

条件码:

条件码寄存器描述了最近的算数或逻辑操作的属性。

CF:进位标志,最高位产生了进位,可用于检查无符号数溢出。

OF:溢出标志,二进制补码溢出——正溢出或负溢出。

ZF:零标志,结果为0

SF:符号标志,操作结果为负。

比较指令:

指令 基于 描述
cmpb S2,S1 S1 – S2 比较字节,差关系
testb S2,S1 S1 & S2 测试字节,与关系
cmpw S2,S1 S1 – S2 比较字,差关系
testw S2,S1 S1 & S2 测试字,与关系
cmpl S2,S1 S1 – S2 比较双字,差关系
testl S2,S1 S1 & S2 测试双字,与关系

访问条件码指令:

指令 同义名 效果 设置条件
sete D setz D = ZF 相等/
setne D setnz D = ~ZF 不等/非零
sets D D = SF 负数
setns D D = ~SF 非负数
setg D setnle D = ~(SF ^OF) & ZF 大于(有符号>
setge D setnl D = ~(SF ^OF) 小于等于(有符号>=)
setl D setnge D = SF ^ OF 小于(有符号<)
setle D setng D = (SF ^ OF) | ZF 小于等于(有符号<=)
seta D setnbe D = ~CF & ~ZF 超过(无符号>)
setae D setnb D = ~CF 超过或等于(无符号>=)
setb D setnae D = CF 低于(无符号<)
setbe D setna D = CF | ZF 低于或等于(无符号<=)

跳转指令:

指令 同义名 跳转条件 描述
jmp Label 1 直接跳转
jmp *Operand 1 间接跳转
je Label jz ZF 等于/
jne Label jnz ~ZF 不等/非零
js Label SF 负数
jnz Label ~SF 非负数
jg Label jnle ~(SF^OF) & ~ZF 大于(有符号>)
jge Label jnl ~(SF ^ OF) 大于等于(有符号>=)
jl Label jnge SF ^ OF 小于(有符号<
jle Label jng (SF ^ OF) | ZF 小于等于(有符号<=)
ja Label jnbe ~CF & ~ZF 超过(无符号>)
jae Label jnb ~CF 超过或等于(无符号>=)
jb Label jnae CF 低于(无符号<)
jbe Label jna CF | ZF 低于或等于(无符号<=)

转移控制指令:(函数调用):

指令 描述
call Label 过程调用,返回地址入栈,跳转到调用过程起始处,返回地址是call后面那条指令的地址
call *Operand
leave 为返回准备好栈,为ret准备好栈,主要是弹出函数内的栈使用及%ebp