先回顾上文,我们已经搞清楚了指针的概念,下面我们先看一串关于数组的代码。
int age[3] = { 21,35,57 };
int* p = &age;
int* a = age;
printf("%d\n",*p);
printf("%d\n",*a);
注意查看关键:
int *p=&age
int *a=age
没错,都是指针,只不过赋予的值一个有&取地址符,而另一个没有。大家思考猜想一下,我们的代码最终会输出什么
没错,都是21,也就是age数组的第一个值,这其中我们会有两个疑问。
第一个:我们指向的都是age数组,可为什么输出的却只有第一个元素21呢?
答案:当你使用 int *p = &age;
时,p
实际上被赋值为 age
数组的地址。由于 age
是一个数组名,会被转换为其第一个元素的地址,因此 p
实际上指向了 &age[0]
。
在这种情况下,无论是 int *p = &age;
还是 int *a = age;
,当你执行 *p和*a
时:
- 如果 a 被初始化为
age
,那么*a
是age[0]
,即21
。 - 如果
p
被初始化为&age
,因为p
的类型是指向整个数组的指针,*p
的结果是数组的第一个元素的引用。这个表达式还是指向数组的第一个整数,最后输出的值也是21
。
第二个问题:为什么两者得到的结果都会是21?
其实这个答案在第一个问题中已经解答过了,int *a=age,其中a指向的是数组age的首个元素,解引用后得到的则是21。
int *p=&age,虽然指向的是整个数组的地址,*p
的结果是数组的第一个元素的引用,在解引用得到的还是该数组的首元素,所以依然输出数组age的首个元素21。
用指针来修改数组的数据:
这很简单,我们要知道,数组都有下标,我们可以根据下标来修改数组的数据:
//修改数组
int age[3] = { 21,35,57 };
int* p = &age;
*p = 8;//修改的第一个数据
*(p + 1) = 9;//修改第二个数据
*(p + 2) = 10;//修改第三个数据
//printf("%d\n", *p);
for (size_t i = 0; i < 3; i++)
{
printf("%d\n", age[i]);
}
return 0;
输出结果如下图:
这样就修改成功了。
数组的数据在内存中是挨个存放的
我记得我在之前发的章节中有讲,接下来我们用代码来证明一下这个结论!
int main()
{
int age[3] = { 1,3,5 };
int* p = age;
getchar();
return 0;
}
结果如下图:
因为int类型为4字节,所以我设置的为4字节 ,方便查看,由图片的内存数据可知,我们数组的数据存在内存中的位置是挨个存放的。
函数指针:
1. 定义函数指针
函数指针是一种指向函数的指针类型。它可以存储函数的地址,并可以通过该指针调用函数。函数指针的定义格式如下:
返回类型 (*指针名)(参数类型1, 参数类型2, ...);
2. 声明和初始化函数指针
#include <stdio.h>
// 函数定义
void sayHello() {
printf("Hello, World!\n");
}
int add(int a, int b) {
return a + b;
}
int main() {
// 声明一个指向没有参数且返回void的函数的指针
可以直接使用函数名
也可以用取地址符
void (*funcPtr1)() = sayHello;
// 声明一个指向接收两个int参数并返回int的函数的指针
int (*funcPtr2)(int, int) = add;
// 通过指针调用函数
funcPtr1(); // 输出: Hello, World!
int result = funcPtr2(3, 4);
printf("Result: %d\n", result); // 输出: Result: 7
return 0;
}
相信大家发现了问题,我在上一张说到,赋值给指针的是内存地址,一般情况就需要在值的前面添加&(取地址符)但是,为什么我在上述代码中,并没有使用取地址符,原因很简单:
在 C 中,一个函数的名称会自动转换为指向该函数的指针。
也就是说void (*funcPtr1)() = &sayHello;等价于void (*funcPtr1)() = sayHello;
当然,大家可能还会有疑问,在上一章节中我们知道了指针需要解引用来拿到指针的值,可是为什么上述代码中的函数没有用到呢?也很简单!
因为 C 语言会自动处理这种情况,C 语言会自动将函数指针转换成可调用的函数。
也就是说,上述代码中的
funcPtr1() 等价于 (*funcPtr1)()
只是这样会使代码更加简洁,同时也希望大家可以写代码时能更加简洁,不然代码太繁杂,写项目时,到后期出了一点小问题时会很麻烦哦
接下来就是给大家查漏补缺的时候了!
我给大家一个简单的代码方便查看:
void test(int a,int b)
{
printf("123 %d %d",a,b);
}
//函数,代码的入口函数
int main() {
//语法:函数类型(*函数名)(参数类型1,参数类型2....)=某个函数
void (*myfunc)() = test;
myfunc(8,9);
getchar();
return 0;
}
大家能看出我传了两个参数给test,输出结果大家都知道会是123 8 9。
这时如果我给test传三个参数,甚至更多参数,会不会对结果有影响?
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test(int a,int b)
{
printf("123 %d %d",a,b);
}
//函数,代码的入口函数
int main() {
//语法:函数类型(*函数名)(参数类型1,参数类型2....)=某个函数
void (*myfunc)() = test;
myfunc(8,9,10,11,12,13);
getchar();
return 0;
}
答案当然是:没有影响!
接下来继续拓展,我们由浅到深:
如果说我参数传多了,那么我们能不能获取这些参数呢?
答案是能!
我们在开头说数组时,已经说过了,数组是怎么存放的?
没错,数组是连续存放的!
我们重新看代码:
#include <stdio.h>
void myfunc(int a, int b) {
printf("123 %d %d\n", a, b);
}
int main() {
void (*pfunc)() = myfunc;
pfunc(1, 3, 4);
getchar();
return 0;
}
我们在学习数组指针时说到了,如果要取值,我们需要怎么取?
没错!就是下标。(p+1)(p+2)对不对
那在test函数中是不是也能用这个方法取出地址
也就是说4的地址为&b+1
这时我们便拿到了数据10的地址,这时我们要怎么通过地址来吧地址的数值拿到?
没错*(解引用)
所以,代码如下:
#include <stdio.h>
void myfunc(int a, int b) {
printf("123 %d %d\n", a, b);
printf("%d\n", *(&b + 1));
}
int main() {
void (*pfunc)() = myfunc;
pfunc(1, 3, 4);
getchar();
return 0;
}
这时,我们便拿到了4的数值。
重要注意事项
- 未定义行为: 访问
*&b + 1
的值可能会依据编译器、编译选项和内存布局等因素造成未定义行为,这意味着输出可能并不总是可靠。 - 在某些情况下,此行为可能返回一个未定义的值,或者在不同环境中产生不同的输出。尽量避免使用这种方法获取额外参数。
所以这个是不可取的,可以碰碰运气。
接下来我们继续拓展:
更改函数地址,变更程序执行流程:
可以通过函数指针来实现。下面是一个简化的例子,展示了如何使用函数指针在 C 语言中动态选择要执行的函数。
#include <stdio.h>
// 定义两个不同的函数
void functionA() {
printf("Function A executed\n");
}
void functionB() {
printf("Function B executed\n");
}
int main() {
// 定义一个函数指针
void (*funcPtr)();
// 根据条件选择执行的函数
int choice;
printf("Enter 1 to execute Function A or 2 to execute Function B: ");
scanf("%d", &choice);
// 更改函数指针的地址
if (choice == 1) {
funcPtr = functionA; // 指向 functionA
} else if (choice == 2) {
funcPtr = functionB; // 指向 functionB
} else {
printf("Invalid choice\n");
return 1; // 返回1表示错误
}
// 调用所选函数
funcPtr(); // 根据指针调用
return 0;
}
同时给大家分享一个正向与逆向问题:
大家先看代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test1()
{
printf("123\n");
};
void test2()
{
printf("321\n");
};
int main()
{
void(*myfunc)();
myfunc = test1;
myfunc();
myfunc = test2;
myfunc();
getchar();
return 0;
};
很简单对不对,我在代码中先给指针函数myfunc赋值了test1,它输出了123。
但是我在之后马上又给myfunc赋值了test2,它马上又输出了321。
这是不是给了我们一个启发:
我们在用什么软件时,我想跳过这个程序,直接输出另一个,那我们是不是可以直接赋其他值给这个指针函数。
我们重新修改代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void test()
{
printf("123\n");
};
void test1()
{
printf("321\n");
};
int main()
{
void(*myfunc)() = test;
test();
test1();
myfunc;
myfunc = test1;
myfunc();
getchar();
return 0;
};
我们接下来看看反汇编:
将test赋值给myfunc的过程看到了吗?
将地址00007FF6534D13E3赋值给了myfunc函数。
但是,如果这时我不要test赋值的地址,那我是不是能把test1的地址00007FF6534D13A7拿来,将test1的地址赋值给myfunc函数。
这些有什么用呢,证明了正向开发中的可行性,和逆向技术的安全性保障。
并且能在D3D HooK透视中有应用
顺便给大家说一下汇编的几个常用的指令:
-
call
:- 用途: 用于调用一个函数。它会将当前指令的下一条指令的地址推入栈中,然后跳转到被调用函数的地址。
- 例子:
call some_function
会将控制流转移到some_function
的入口地址。
-
lea
(Load Effective Address):- 用途: 计算一个内存地址,并将其存储到寄存器中。它不会访问指定的内存地址,仅仅进行地址的计算。
- 例子:
lea rax, [rbx + 8]
将rbx + 8
的地址计算结果加载到rax
中。
-
nop
(No Operation):- 用途: 不执行任何操作。常用于占位或填充代码行,保持程序的同步,特别是在需要调整指令位置或替换指令时。
- 例子: 简单地写
nop
不会对程序的逻辑产生任何影响。
-
mov
:- 用途: 将数据从一个位置复制到另一个位置。可以是将寄存器中的值移动到另一个寄存器,也可以将内存中的值加载到寄存器,反之亦然。
- 例子:
mov rax, rbx
将rbx
中的值复制到rax
中。mov [rbp+8], rax
将rax
中的值存储到栈位置[rbp+8]
。