滴水逆向P16-20150126(C语言数据类型)

裸函数:(注意gcc不支持这个,裸函数也是C语言内嵌汇编,因此后面给出vscode+gcc支持的C语言内嵌汇编方式,大概就是AT&T汇编+某些支持的关键字)

链接转载自:裸函数-CSDN博客

_declspec(naked), 就是告诉编译器,在编译的时候,不要优化代码,不要随便反汇编, 通俗的说就是, 没代码,完全要自己写

使用__declspec(naked)关键字定义函数:

1,使用 naked 关键字必须自己构建 EBP 指针 (如果用到了的话);

2,必须自己使用 RET 或 RET n 指令返回 (除非你不返回);

_delcspec(naked)用在驱动编写,C语言内嵌汇编完成一些特定功能。wwwww

 举例1:空函数
1 #define NAKED __declspec(naked)
2 
3 void NAKED code(void)
4 {
5     __asm
6     {
7         ret
8     }
9 }


 举例2:无参数无返回值的函数框架
 1 void __declspec(naked) plus()
 2 {
 3     __asm
 4     {
 5         //提升堆栈
 6         push ebp
 7         mov ebp,esp
 8         sub ebp,0x40
 9         //保护现场
10         push ebx
11         push esi
12         push edi
13         //向缓冲区填充数据
14         lea edi,dword ptr ds:[ebp-0x40]
15         mov eax,0xCCCCCCCC
16         mov ecx,0x10
17         rep stosd  ;rep stos dword ptr es:[edi]
18         //恢复现场
19         pop edi
20         pop esi
21         pop ebx
22         //降低堆栈
23         mov esp,ebp
24         pop ebp
25         //返回函数调用前的下一行地址
26         ret
27     }
28 }

 举例3:有参数有返回值的函数框架
 1 int __declspec(naked) plus(int x, int y)
 2 {
 3     __asm
 4     {
 5         //提升堆栈
 6         push ebp
 7         mov ebp,esp
 8         sub esp,0x40
 9         //保护现场
10         push ebx
11         push esi
12         push edi
13         //向缓冲区填充数据
14         lea edi,dword ptr ds:[ebp-0x40]
15         mov eax,0xCCCCCCCC
16         mov ecx,0x10
17         rep stos dword ptr es:[edi]
18 
19         //函数核心功能块----------------------重点看,比上面多出来的部分
20         mov eax,dword ptr ds:[ebp+0x8]
21         add eax,dword ptr ds:[ebp+0xC]
22 
23         //恢复现场
24         pop edi
25         pop esi
26         pop ebx
27 
28         //降低堆栈
29         mov esp,ebp
30         pop ebp
31         //返回函数调用前的下一行地址
32         ret
33     }
34 }

 举例4:带局部变量的函数框架
 1 int __declspec(naked) plus(int x, int y)
 2 {
 3     __asm
 4     {
 5         //提升堆栈
 6         push ebp
 7         mov ebp,esp
 8         sub esp,0x40
 9         //保护现场
10         push ebx
11         push esi
12         push edi
13         //向缓冲区填充数据
14         lea edi,dword ptr ds:[ebp-0x40]
15         mov eax,0xCCCCCCCC
16         mov ecx,0x10
17         rep stos dword ptr es:[edi]
18 
19         //局部变量入栈-----------------------就是传入局部变量
20         mov dword ptr ds:[ebp-0x4]
21         mov dword ptr ds:[ebp-0x8]
22 
23         //函数核心功能块-------------------------就是函数功能
24         mov eax,dword ptr ds:[ebp+0x8]
25         add eax,dword ptr ds:[ebp+0xC]--------------eax就是返回值
26 
27         //恢复现场
28         pop edi
29         pop esi
30         pop ebx
31         //降低堆栈
32         mov esp,ebp
33         pop ebp
34         //返回函数调用前的下一行地址
35         ret
36     }
37 }

上面几个例子能归纳出个大概结构,当常识背下来最好:

共6步--提升堆栈,保存,  填充+填充核心功能  ,取出保存,退栈

__stdcall,__cdecl,__fastcal这是什么玩意,属于哪些领域的知识,应该在哪里学习,怎样可以找到学习资料

__stdcall、__cdecl、和__fastcall是C和C++编程语言中与函数调用约定(calling conventions)相关的关键字。这些关键字指定了函数参数如何被传递给函数,以及谁(调用者或被调用者)负责从栈上清除参数。这些调用约定对于编写操作系统级别的代码、与操作系统API交互、或者编写需要与其他语言互操作的库尤其重要。

__cdecl:是C和C++中的默认调用约定。在这种约定中,调用者负责从栈上清除参数。这允许函数具有可变数量的参数。

__stdcall:是Windows API的标准调用约定。在这种约定中,被调用的函数负责从栈上清除参数。这种约定在编译为DLL时有利于减小生成的可执行文件的大小。

__fastcall:在这种约定中,函数的前两个整数或指针参数通过寄存器传递,而不是通过栈。这旨在提高函数调用的速度。不过,具体的寄存器使用细节可能依编译器和目标架构的不同而有所差异。

学习资料和资源

这些概念主要属于低级编程、系统编程和编译器设计领域的知识。了解这些概念的最佳方式是通过以下资源:

编译器文档:查阅您使用的编译器的官方文档,比如GCC、Clang或MSVC,这些文档通常会详细介绍支持的调用约定。

操作系统文档:由于__stdcall和__fastcall特别是在Windows编程中使用,因此Microsoft的官方文档是一个很好的资源。

专业书籍:关于C/C++编程、Windows编程、操作系统设计和编译器设计的书籍通常会涵盖这些主题。书籍如《Windows程序设计》(第五版)以及《C专家编程》都是很好的资源。

在线教程和课程:许多在线平台和网站提供了关于系统编程的教程和课程,这些也可能涵盖函数调用约定的主题。

开源代码:查看使用这些调用约定的开源项目的源代码也是一个很好的学习方法。例如,Windows的开源API或与操作系统交互的库。

了解和使用这些调用约定通常需要对底层计算机架构(如CPU寄存器)和操作系统的工作方式有基本的理解。因此,这也是学习计算机科学和软件工程基础的一部分。

课程里面的举例:(注意,实际会因版本等各种原因,各种调用约定的方式会有所不同,比如我在vscode里用c/c++拓展,gcc编译,调试时候都没见过外部平栈,三种调用约定全部在内部平栈,用的参数更多,但是可能现在能用的寄存器更多了,所以根本不需要push,都是直接存到寄存器里面,或许旧一点的程序使用push,别忘了滴水的课是2015年的,用的还是VC6)

常见的几种调用约定:
调用约定参数压栈顺序平衡堆栈
__cdecl从右至左入栈调用者清理栈
__stdcall从右至左入栈自身清理堆栈
__fastcallECX/EDX传送前两个自身清理堆栈
剩下:从右至左入栈
1、int __cdecl Plus(int a, int b)
{
return a+b;
}
push        2
push        1
call        @ILT+15(Plus) (00401014)
add         esp,8
2、int __stdcall Plus(int a, int b)
{
return a+b;
}
push        2
push        1
call        @ILT+10(Plus) (0040100f)
函数内部:
ret         8
3、int __fastcall Plus(int a, int b)
{
return a+b;
}
mov         edx,2
mov         ecx,1
call        @ILT+0(Plus) (00401005)
函数内部:
ret         
4、int __fastcall Plus4(int a, int b,int c,int d)
{
return a+b+c+d;
}
push        4
push        3
mov         edx,2
mov         ecx,1
call        @ILT+5(Plus) (0040100a)
函数内部:
ret         8

C语言内嵌汇编主流方式:

附:了解C语言底层,反汇编的一个文档,有比较全面的例子:

https://github.com/1184893257/simplelinux/blob/master/README.md

内嵌汇编(Inline Assembly)存在于多种编程语言中,尤其是C和C++,用于在高级语言代码中直接嵌入低级汇编代码。有不同的内嵌汇编语法主要是因为不同的编译器设计者选择了不同的方式来实现这一功能,同时也反映了不同硬件架构和操作系统的特定需求

主流内嵌汇编的方式

一、GNU GCC内嵌汇编

使用__asm__关键字或简写的asm。

支持扩展的汇编语法(Extended Assembly),允许在汇编代码中包含输入、输出和破坏描述(Clobber)。

采用AT&T语法

二、Microsoft Visual C++内嵌汇编

使用__asm关键字。

直接在C或C++代码中书写汇编语句。

采用Intel语法

仅在32位版本的MSVC中支持。对于64位代码,推荐使用外部汇编器或汇编库。

Intel编译器内嵌汇编

与Microsoft的风格类似,但旨在跨平台支持,也采用Intel语法。

为什么会有不同的内嵌汇编方式

兼容性:编译器设计者需要确保他们的内嵌汇编方式能在其支持的平台上运行。例如,GCC需要在多种操作系统和硬件架构上工作,而MSVC主要针对Windows。

语法偏好:AT&T语法与Intel语法在语法上有显著不同。一些开发者和系统可能已经对一种语法有所偏好,编译器提供相应的内嵌汇编方式以满足这些需求。

功能需求:不同的应用场景和功能需求可能会导致不同的内嵌汇编设计。例如,GCC的扩展汇编提供了强大的功能,允许开发者详细指定汇编指令的输入、输出和副作用,这在进行高度优化时非常有用。

历史原因:随着时间的推移,不同的编译器和技术栈发展出了自己的特色和用户基础,导致了不同内嵌汇编语法的共存。

综上所述,不同的内嵌汇编方式的存在反映了软件开发领域的多样性和复杂性,以及在满足不同技术需求和用户习惯方面的努力。开发者在使用内嵌汇编时应考虑到这些差异,选择最适合自己项目和目标平台的工具和语法。

GNU GCC反汇编语法学习:(仅供学习底层c语言用,方便看细节,反汇编大型项目使用像IDA Pro、Ghidra、Radare2的专业工具效率最高

附:还是这个老哥的文档:https://github.com/1184893257/simplelinux/blob/master/gcc.md

C语言的编译过程通常可以分为四个主要步骤:预处理、编译、汇编和链接。每个步骤都对应GCC的不同指令:

预处理(Preprocessing):

作用:处理源代码文件中的预处理指令,如宏定义(#define)、条件编译(#ifdef, #endif)和文件包含(#include)等。

指令:gcc -E yourprogram.c -o yourprogram.i

这个指令将只执行预处理,输出的yourprogram.i文件包含了预处理后的代码。

编译(Compilation):

作用:将预处理后的源代码转换成汇编语言代码。

指令:gcc -S yourprogram.i -o yourprogram.s

这个指令将.i文件(预处理后的文件)编译成汇编语言.s文件。

汇编(Assembly):

作用:将汇编语言代码转换成机器语言代码,生成目标文件。

指令:gcc -c yourprogram.s -o yourprogram.o

这个指令将.s文件(汇编语言文件)汇编成.o文件(目标文件)。

链接(Linking):

作用:将一个或多个目标文件与库文件链接在一起,生成最终的可执行文件。

指令:gcc yourprogram.o -o yourprogram

这个指令将.o文件(目标文件)与所需的库文件链接,生成最终的可执行文件yourprogram。

如果要一步完成整个编译过程(从源代码到最终的可执行文件),可以直接使用以下指令:

指令:gcc yourprogram.c -o yourprogram

这条指令会自动执行上述提到的所有步骤,最终生成名为yourprogram的可执行文件。这种方式简化了编译流程,适用于大多数情况,尤其是项目不是非常复杂时。对于复杂项目,可能需要分步执行,以便更好地控制编译过程和调试。

-o参数用来指定生成的文件名称

用ojdump反汇编出指定的内容,在终端显示:

先用nm p17_2.exe或者objdump -t p17_2.exe查看程序的符号表找到main的地址

再用objdump -d p17_2.exe --start-address=0xADDRESS --stop-address=0xADDRESS

反汇编出main地址处的内容

GNU GCC内嵌汇编的语法学习:

附:还是:https://github.com/1184893257/simplelinux/blob/master/inlineasm.md

User

那么你能帮我在这段代码的基础上重写一段吗,要能使用三次ecx

void *memcpy(void *dest, const void *src, size_t n)

{

       int d0, d1, d2;

       asm volatile(

              "rep ; movsl\n\t"

              "movl %4,%%ecx\n\t"

              "rep ; movsb\n\t"

              : "=&c" (d0), "=&D" (d1), "=&S" (d2)

              : "0" (n >> 2), "g" (n & 3), "1" (dest), "2" (src)

              : "memory");



       return dest;

}

ChatGPT

如果要构造一个场景,使得可以三次使用ECX,我们可以考虑一个简化的例子,其中我们先移动四字节数据块,然后移动剩余不足四字节的数据,最后假设还需要对一些数据执行另外的操作(这里我们假设是再移动一次特定数量的字节,只是为了示例)。

void *memcpy(void *dest, const void *src, size_t n, size_t extra_bytes)

{

    int d0, d1, d2;

    asm volatile(

        "rep movsl\n\t"             // 首先移动n >> 2次四字节数据

        "movl %4, %%ecx\n\t"        // 设置ECX为n & 3,准备移动剩余的单字节数据

        "rep movsb\n\t"             // 移动剩余不足4字节的数据

        "movl %5, %%ecx\n\t"        // 假设我们还需要移动额外的某些字节,%5为这次操作的计数

        "rep movsb\n\t"             // 执行这次额外的字节移动

        : "=&c" (d0), "=&D" (d1), "=&S" (d2) // 输出约束

        : "0" (n >> 2), "g" (n & 3), "g" (extra_bytes), "1" (dest), "2" (src) // 输入约束

        : "memory"); // 影响的状态



    return dest;

}

在这个修改后的版本中,我们加入了一个额外的参数extra_bytes,这代表了除了原本基于n进行的数据移动之外,我们还想进行一次额外的数据移动操作,其长度由extra_bytes确定。

User

那么问题来了,我怎么知道%4和%5引用的是"g" (n & 3)还是 "g" (extra_bytes)

ChatGPT

在内联汇编中,%数字 的使用是用来引用输入和输出操作数的位置。数字是根据出现在汇编指令中的顺序来确定的,而不是基于它们在输入或输出列表中的类型或约束。输入操作数紧随输出操作数之后进行编号

在你的例子中,按照声明的顺序,操作数被编号如下:

输出操作数没有直接使用%数字的形式引用,因为它们是由编译器根据约束选择寄存器的。输出操作数占据的位置编号从0开始,但在实际的汇编代码中,我们不直接通过%0、%1这样的形式来引用它们。

输入操作数跟随输出操作数进行编号。在这个例子中,你首先有"0" (n >> 2),这实际上是通过"=&c" (d0)输出约束引用的,因为"0"这个约束告诉编译器这个输入操作数应该使用与第一个输出(d0)相同的寄存器,所以它没有单独的编号。

以下是具体的编号方式:

"0" (n >> 2) 被隐式编号为 %0,因为它被约束为与第一个输出操作数使用相同的寄存器。

"g" (n & 3) 是第一个显式的输入操作数,所以它是 %4。

"g" (extra_bytes) 是第二个显式的输入操作数,所以它是 %5。

"1" (dest) 和 "2" (src) 分别引用的是通过特定的约束指定寄存器的输出操作数,它们不是通过%数字的形式在汇编代码中直接引用的。

因此,当你看到汇编代码中的 %4 和 %5:

%4 引用的是 "g" (n & 3),即表示应该将 n & 3 的结果放入 ECX 寄存器以进行第二次复制操作的字节计数。

%5 引用的是 "g" (extra_bytes),这是你额外加入的,用于第三次操作的字节计数。

这种编号方式确保了你能够精确控制每个操作数如何被汇编代码使用,即便是在复杂的内联汇编表达式中。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值