C语言学习day17:指针数组的应用与函数指针以及正向、逆向的概念

先回顾上文,我们已经搞清楚了指针的概念,下面我们先看一串关于数组的代码。

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透视中有应用

顺便给大家说一下汇编的几个常用的指令:

  1. call:

    • 用途: 用于调用一个函数。它会将当前指令的下一条指令的地址推入栈中,然后跳转到被调用函数的地址。
    • 例子call some_function 会将控制流转移到 some_function 的入口地址。
  2. lea (Load Effective Address):

    • 用途: 计算一个内存地址,并将其存储到寄存器中。它不会访问指定的内存地址,仅仅进行地址的计算。
    • 例子lea rax, [rbx + 8] 将 rbx + 8 的地址计算结果加载到 rax 中。
  3. nop (No Operation):

    • 用途: 不执行任何操作。常用于占位或填充代码行,保持程序的同步,特别是在需要调整指令位置或替换指令时。
    • 例子: 简单地写 nop 不会对程序的逻辑产生任何影响。
  4. mov:

    • 用途: 将数据从一个位置复制到另一个位置。可以是将寄存器中的值移动到另一个寄存器,也可以将内存中的值加载到寄存器,反之亦然。
    • 例子mov rax, rbx 将 rbx 中的值复制到 rax 中。mov [rbp+8], rax 将 rax 中的值存储到栈位置 [rbp+8]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值