• 《老码识途》读书笔记:第一章(上)


    《老码识途》读书笔记:第一章--欲向码途问大道,锵锵bit是吾刀(上)

       1、赋值语句

    对于全局变量赋值语句,例如下面这句:

    1 int gi;
    2 void main(int argc, char* argv[]) 
    3 {
    4     gi = 12;   
    5 }

    对于gi = 12;这句赋值语句来说,可查看其汇编表示形式为(内存地址为书中例子):

    1 0041138E        mov     dword ptr ds:[00417140h],0ch

    其中00417140h为十六进制数表示的全局变量gi存放在内存中的地址,0ch是十进制数12的十六进制表示,0041138E为十六进制数表示的赋值指令mov在内存中的存放地址。这句汇编指令的意思是,将十六进制数0ch以四个字节(dword)的形式存放入从内存地址00417140h开始的四个字节长度的内存空间中。再来观察其对应的机器码如下:

    1 c7 05 40 71 41 00 0c 00 00 00

    其中c7 05 代表mov指令,40 71 41 00 代表地址00417140h(小数端存储方式),0c 00 00 00 代表以四个字节表示的要进行赋值的数12。假设从内存地址00417140h开始的十个字节分别为:

    1 0x00417140    11 11 11 11 11 11 11 11 11 11

    则该条赋值语句执行完毕后该内存中的值应该为:

    1 0x00417140    0c 00 00 00 11 11 11 11 11 11

    因为一次修改了四个字节的内存空间,且小端机在内存中的字节数据是倒序存放的,因此前四个字节变成了0c 00 00 00。

    如果要修改赋值语句的机器码,例如将上面语句中的12改为894567,则需先求出894567的十六进制表示为0xda667。同时还要考虑到小端机内存字节数据倒序存放的特点,即可完成对赋值语句字节码的修改,修改后的机器码如下:

    1 c7 05 40 71 41 00 67 a6 0d 00

     根据上面的分析我们知道指令不过就是一些字节的组合,因此我们可以抛开C语言,自己在内存中构造指令来执行。具体思路为可以先在内存中分配一块区域,存放我们要执行的指令的机器码。然后在正常的函数中通过jmp指令跳转到存放我们构造的指令所在的内存地址,因为控制读取指令的EIP寄存器中的值总是指向当前指令之后的内存地址,因此还必须在我们构造的指令之后再多构造一条jmp指令,使得程序在执行完构造的指令后还能通过jmp指令跳转回原程序中,其主函数代码如下:

     1 void main()
     2 {
     3     void* code = buildCode();
     4     _asm {
     5         mov        address, offset _lb1
     6     }
     7 
     8     gi = 12;
     9     printf("gi = %d\n", gi);
    10     _asm     jmp code             // 跳转到相应的内存地址去执行构造的指令
    11     gi = 13;
    12     
    13 _lb1:
    14     printf("gi = %d\n", gi);     // 打印的结果为18而不是13
    15     getchar();
    16 }

    其中第3行调用buildCode获取新构建代码的首地址,第4到6行将第13行代码的地址赋值给address用以在执行完新构建的代码之后返回原函数,第10行跳转到指针code指向的地址。我们将在新构建的代码中将变量gi赋值为18,并在第14行打印这个赋值后的结果。(显而易见第11行的赋值语句就这样被跳过忽略了,可怜的孩纸)。

    然后就是真正构建指令的过程了,mov指令的机器码我们之前已经了解过了,同样地通过反汇编我们同样能看到jmp指令的机器码格式为:

    1 ff 25 12 34 56 78

    其中ff 25是jmp指令的机器码表示,后面的四个字节则是要跳转的目标内存地址的十六进制表示。因此两条指令总共需要16字节的内存空间,然后分别将对应的机器码存入到这些内存空间中,代码如下所示:

     1 void* buildCode() 
     2 {
     3     char* code = (char *)malloc(16);
     4     char* pMov = code;
     5     char* pJmp = pMov + 10;
     6     char* pAddress;
     7 
     8     //mov gi, 18
     9     pMov[0] = 0xc7;
    10     pMov[1] = 0x05;
    11     pAddress = pMov + 2;
    12     *((int *)pAddress) = (int)&gi;
    13     *((int *)(pAddress + 4)) = 18;
    14 
    15     //jmp address
    16     pJmp[0] = 0xff;
    17     pJmp[1] = 0x25;
    18     *((int *)(&pJmp[2])) = (int)&address;
    19 
    20     return code;
    21 }

     在上面的代码中,首先使用malloc函数分配一块长度为16字节的内存空间。然后将该内存空间划分为两部分,前一部分存放mov指令的机器码,后一部分存放jmp指令的机器码。在第9到13行,将mov指令的机器码按字节存入内存中。在16到18行,将jmp指令的机器码存放在紧邻mov指令之后的内存空间中。最后返回该内存空间的首地址供主函数进行跳转。

       2、理解指针和指针强制转换

    学习过C/C++的人都知道指针的值其实就是一个内存地址,但是指针同时又有类型的区别,例如int* 和 float*。那么为什么区区一个内存地址还要有类型的区别呢?编译器怎么判断一个指针的类型?这些有关于类型的信息究竟存储在什么地方呢?来看一看下面的这一段代码:

    1 int gi;
    2 int *pi = NULL;
    3 void main() 
    4 {
    5 
    6     pi = &gi;
    7 
    8     *pi = 12;
    9 }

    对其中的pi = &gi; 这一赋值语句,与其对应的汇编代码为:

    1 00411452        mov     dword ptr ds:[00417164h], 417168h

    其中00417164h是pi的内存地址,417168h是gi的内存地址,因此这条赋值语句的作用是获取全局变量gi所在的内存地址并将该地址赋值给指针pi,即将gi的地址放入一个4字节的变量pi中(书中原话)。因为变量gi的地址长度刚好为4个字节,所以指针确实只存储了变量的地址,那么指针的类型信息究竟储存在什么地方呢?

     再来看看 *pi = 12 这一句的汇编代码如下:

    1 0041145C        mov     eax, dword ptr ds:[00417164h]
    2 00411461        mov     dword ptr [eax], 0ch

     在第二条语句中,除了要赋的值(0ch)、被赋值的地址外,还有一个dword符号。是它回答了我们的问题:“写几字节?”dowrd表明将在内存中写入四个字节的信息,因此指针的类型信息决定了赋值/读取时写/读多少字节。

      读/写多少字节的信息不是存放在指针变量中,而是放到了与该地址相关的赋值指令中,mov指令中的dword指明了这个信息。

    为了验证上面的结论,再来看下面这段赋值语句的汇编代码:

     1 short gi;
     2 short *pi;
     3 
     4 int main() {
     5     pi = &gi;
     6     00413762        mov     dword ptr ds:[417165h], 417168h
     7 
     8     *pi = 12;
     9     0041376C        mov     eax, 0ch
    10     00413771        mov     ecx, dword ptr ds:[417164h]
    11     00413777        mov     word ptr ds:[ecx], ax
    12 }

     *pi = 12 对应的三条指令进行的操作分别为:

    1、mov  eax, 0ch:将12放入eax中,eax为4字节,12存放在eax的低2字节即ax中。

    2、mov  ecx, dword ptr ds:[417164h]:将pi存储的地址即gi的地址放入ecx中(pi的地址是417164h,[417164h]中存储的是gi的地址)。

    3、mov   word ptr ds:[ecx], ax:将eax的低2字节存储的内容(就是要赋值的12)存入ecx指向的地址(即gi的地址)中。"word"表明了如果向gi所在地址存储,将写入2字节。

     根据上面的分析可知,指针类型信息short*体现在赋值指令mov中,而不是存放在指针变量中,指针变量只存放了地址。C语言的指针类型包含两方面信息:一是地址,存放在指针变量中;二是类型信息,关乎读写的长度,没有存储在指针变量中,位于用该指针读写是的mov指令中,不同的读写长度对应的mov指令不同。

     C语言之所以要包装出指针的概念,是在汇编地址的内涵上增加了另一层含义,即读/写多少字节。不同类型指针,访问字节数不同。int*访问4字节,short*访问2字节,char*访问1字节。这样就方便我们操控一个地址,否则如果只有地址信息,每次访问它还要附加说明访问的字节数。同时,指针的加减也并不是简单地只是加减1字节,而是与其每次能访问的字节长度有关。例如int*加一是加4字节,而short*加一则是加2字节。

    关于指针的强制类型转换的问题,联系上面的知识很容易就能够明白,先来看下面的代码:

     1 int i;
     2 int *pi;
     3 short *ps;
     4 char *pc;
     5 
     6 void main(int argc, char* argv[]) 
     7 {
     8     pi = &i;
     9     0041138e        mov     dword ptr ds:[417148h], 41714ch
    10 
    11     ps = (short *)&i;
    12     00411398         mov     dword ptr ds:[417144h], 41714ch
    13 
    14     pc = (char *)&i;
    15     004113a2         mov     dword ptr ds:[417140h], 41714ch
    16 }

    从上面的代码可以看出,只有赋值地址的三条指令没有产生任何与类型相关的指令。可知,在指针变量赋值上,强制转换只是编译器的一个善意提醒,没有产生实际的指令。

    指针强制转换的影响不是在转换的时候发生,而是在用转换的身份去访问内存时体现到了指令中,例如下面的代码:

     1 *pi = 0x1234;
     2 004113ac         mov     eax, dword ptr ds:[417148h]
     3 004113b1         mov     dword ptr [eax], 1234h
     4 
     5 *ps = 0x1234;
     6 004113b7         mov     eax, 1234h
     7 004113bc         mov     ecx, dword ptr ds:[417144h]
     8 004113c2         mov     word ptr [ecx], ax
     9 
    10 *pc = 0x12;
    11 004113c5         mov     eax, dword ptr ds:[417140h]
    12 004113ca         mov     byte ptr [eax], 12h

     从上面的代码可以看出,在用之前经过强制转换之后赋值的不同类型的指针,虽然指向的都是同一个内存地址,但是其可操作的内存空间大小却是不一样的。要考虑什么情况下强制转换是安全的,就要看用这个转换后的身份去访问内存是否安全,简单说有以下原则:

       如果转换后指针指向的数据类型大小小于原数据类型大小,那么用该转化后的指针访问就不会越过原数据的内存,是安全的,否则危险,要越界。

    在上面的例子中, ps = (short *)&i; 强制转换后,用ps来访问内存是2字节,而i本身是4字节,所以不会越界,是安全的。而下面的代码就是危险的:

    1 short s;
    2 int *p;
    3 p = (int *)&s;

    因为p指向的是short变量s,大小为2字节,而p为整数指针,用它访问指向的内存将生成访问4字节的指令,访问将会越界。

     

     

  • 相关阅读:
    linux常用命令
    虚函数、纯虚函数、虚函数表、虚析构函数(一)
    有没有easyx库文件
    请教那位老师帮忙修重新改按键定义
    C语言txt文件元素追加
    do-while是如何控制指针+1的呢
    printf后的句子怎么显示啊
    如何用C语言生成高斯粗糙面
    读取文件时程序报错调试了好久不知道如何解决
    新人求教:字符串在文件输入中的整体输入
  • 原文地址:https://www.cnblogs.com/ZJAJS/p/2942901.html
Copyright © 2020-2023  润新知