C基础
1、在C
中,函数的变量是从右往左传递的,也就是test(x,y)
,先传入y
,再传x
。
2、变量的分类:
(1)全局变量。在编译的时候就已经确定了内存地址和宽度,变量名就是内存地址的别名。如果不重新编译,那么全局变量的内存地址就会不变,这也就是平时所说的基址
。并且全局变量中的值任何程序都可以改,是公用的。
(2)局部变量。是在函数内部进行声明的,如果函数没有执行的,局部变量就没有内存空间的。局部变量的内存是分配在堆栈中的,程序执行才会分配,也就是局部变量的内存地址是不固定的,所以局部变量只能在函数内部进行使用。
(3)全局变量可以没有初始值而使用使用。局部变量在使用前必须要赋值,因为这里C是通过mov将值传入到堆栈中的,如果不提前赋值的话,堆栈中此时就没有这个局部变量的位置了
当然了,为啥不能在遇到int x
的时候就给他mov
一个值进入呢,这个也是没有想明白。
变量与参数的内存布局,以EBP
为分界,EBP
往上是局部变量从EBP-4
开始,往下是参数区从EBP+8
开始
3、C语言数据类型
- 基本类型
1.整数类型
- char 8bit ,天啊,你居然是整数类型,而不是字符串,
- short 16bit
- int 32bit
- long 32bit
2.浮点类型
- 默认为 double 型 ,64bit ,居然超了32位,哈哈哈,查了一些资料,看着还挺有意思的。
- 声明 float 型常量时,须后加‘f’或‘F’,这个是32bit的。
- 详细说明
- 构造类型
1.数组类型
- 数组作为参数进行传递时,使用的是数组起始成员的地址
2.结构体类型
struct关键字,就是自定义一个数据
3.共用体(联合)类型
- 指针类型
- 空类型
4、数组的溢出攻击,这就是学C
的魅力吗
#include "stdafx.h"
#include <windows.h>
void plusTest(){
while(1){
printf("test");
Sleep(3000);
}
}
int main(int argc, char* argv[])
{
int arr[8];
arr[9] = (int)&plusTest;
return 0;
}
程序在执行完成后,会一直输出test
字符。
通过进入汇编查看
简单说明一下会溢出成功的原因
push ebp
mov ebp,esp
sub esp,60h //提升堆栈
push ebx
push esi
push edi
lea edi,[ebnp-60h]
mov exc,18h
mov eax,0cccccccch
rep stos dword ptr [edi] //向缓冲区中写入数据
mov dword ptr [ebp+4],offset @ILT+5(plusTest) (0040100a) //这里就是关键了,他将获取到的函数地址写入到了ebp+4的位置,正常来说,数组当中最后一个成员应该是ebp-4,也就是缓存区最底下的位置。但是由于引用了一个超出范围的下标,所以就存到了ebp+4的地方,也就是原来存返回地址的地方!
xor eax eax //此时将eax置0
pop edi
pop esi
pop ebx //恢复寄存器的值
mov esp,ebp //恢复esp原有的位置
pop ebp //恢复ebp原有的位置
ret //将eip的值改为esp的值,并且esp+4,这个esp+4,就是之前存了40100a的值的地方,所以CPU下一次执行就会执行到这个地址上。
5、多维数组的结构
所以int arr[3*4]
和 int arr[3][4]
在内存中是一样的,太强了!!!编译器如何查找多维数组的,其实还是换算成一组数组。
6、字节对齐
如果一个变量占用n个字节,则该变量的起始地址必须是n
的整数倍,即:存放起始地址%n=0
。
如果是结构体,那么结构体的起始地址是其最宽数据类型成员的整数倍。
当对空间要求较高的时候,可以通过#pragma pack(n)
来改变结构体成员的对齐方式
#pragma pack(1)
struct Test{
char a;
int b;
}
#prama pack()
C的指针
指针类型
指针也就是和其他数据类型一样的,也就只是一种类型!
任何类型都可以带*
,加上*
以后就是新的类型,统称为指针类型
!这个*
也可以加多个,比如char*、char** 、char***
,他们都是指针类型。指针类型的变量宽度永远都是4
字节,无论具体类型是什么,无论有几个*
。
给指针类型的变量进行赋值
int*** x;
x = (int***)1;
指针类型可以进行自加和自减,带*
类型的变量,++
或者--
新增(减少)的数量是去掉一个*
后变量的宽度。
char*** a;
a = (char***)100;
a++;
//最后a的结果是104
char** a;
a = (char**)100;
a++;
//最后a的结果是104
char* a;
a = (char*)100;
a++;
//最后a的结果是101
指针类型可以进行加减运算,但不可以乘或者除。指针类型变量与其他整数相加时:指针类型变量 + N = 指针类型变量 + N*(去掉一个*后类型的宽度)
char**** a;
a = (char****)100;
a = a + 5;
//此时a结果为120
char*** a;
a = (char***)100;
a = a + 5;
//此时a结果为120
char* a;
a = (char*)100;
a = a + 5;
//此时a结果为105
short* a;
a = (short*)100;
a = a + 5;
//此时a结果为110
int* a;
a = (int*)100;
a = a + 5;
//此时a结果为120
指针还可以做比较,所谓的类型都是给编译器看的,在汇编以后,指针显示的也只是一个无符号数。
&
的使用
&
为取地址符,可以获得任何一个变量所在的地址。&
变量的类型为加上一个*
号的类型
*
的使用
*
的几种用途:(1)乘法运算符 (2)定义新的类型 (3)取值运算符*()
,即取变量地址内的值。*
加指针类型的变量类型为减去一个*
号的类型
int x = 1;
int* p = &x;
printf("%x %x\n",p,*(p))
//p打印的是x的内存地址,*(p)打的是x的内存地址所对应的值1
指针取值的两种方式:arr[i]相当于*(p+i)
结构体指针
例子一:
struct Point{
int x;
int y;
}
void main(){
Point p = {1,2};
Point* px = &p;
int x = px->x; //利用结构体指针读值
px->y = 100; //利用结构体指针修改值
}
例子二:
struct Point{
int x;
int y;
}
void main(){
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
Point* px = (struct Point *)arr;
int x = px->x; //x为1
int y = px->y; //y为2
px++;
int x1 = px->x; //x为3
int y1 = px->y; //y为4
}
//太有意思了
数组指针和指针数组
int *p[5]
和 int (*p)[5]
是两种不同的声明方式,分别代表不同的数据结构。以下是它们的具体解释和区别:
int *p[5]
含义:这是一个数组,数组的每个元素都是一个指向整数的指针。
结构:p
是一个包含 5
个元素的数组,每个元素都是 int *
类型。
int a = 10;
int b = 20;
int *p[5]; // 声明一个包含5个int指针的数组
p[0] = &a; // p[0] 指向 a
p[1] = &b; // p[1] 指向 b
int (*p)[5]
含义:这是一个指针,指向一个包含 5 个整数的数组。
结构:p 是一个指向数组的指针,该数组包含 5 个 int 类型的元素。
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5]; // 声明一个指向包含5个int的数组的指针
p = &arr; // p 指向 arr 数组
总结
int *p[5]
是一个数组,包含5
个指向整数的指针。
int (*p)[5]
是一个指针,指向一个数组,该数组包含5
个整数。
例子,下面是一个简单的示例,展示了这两种声明的使用:
#include <stdio.h>
int main() {
// 示例 1: int *p[5]
int a = 10, b = 20;
int *p1[5];
p1[0] = &a;
p1[1] = &b;
printf("p1[0]: %d\n", *p1[0]); // 输出: 10
printf("p1[1]: %d\n", *p1[1]); // 输出: 20
// 示例 2: int (*p)[5]
int arr[5] = {1, 2, 3, 4, 5};
int (*p2)[5] = &arr;
printf("(*p2)[0]: %d\n", (*p2)[0]); // 输出: 1
printf("(*p2)[1]: %d\n", (*p2)[1]); // 输出: 2
return 0;
}
在这个示例中,可以看到 p1 是一个指针数组,而 p2 是指向一个数组的指针。
调用约定
就是告诉编译器,参数怎么传递,结果怎么返回,堆栈怎么平衡的!
调用约定 | 参数压栈顺序 | 平衡堆栈 |
---|---|---|
__cdecl | 从右至左入栈 | 调用者清理堆栈 |
__stdcall | 从右至左入栈 | 自身清理堆栈 |
__fastcall | ECX/EDX传送前两个,其余的从右至左入栈 | 自身清理堆栈 |
函数指针
终于理解这里了,原来这就相当于frida
里面的nativefunction
,用来调用原程序中的原生函数。
int (__cdecl* *pFun)(INT skill, INT obj, INT arg);
pFun = (int (__cdecl* *pFun)(INT skill, INT obj, INT arg))0x12345678;
pFun(0,0,0); //假如0x12345678是技能的函数地址,那么执行pFun就相当于调用了此地址了
预处理
预处理一般是指在程序源代码被转换为二进制代码之前,由预处理器对程序源代码文本进行处理,处理后的结果再由编译器进一步编译。
预处理功能主要包括宏定义
、文件包含
、条件编译
三部分。
1. 宏定义 (#define)
宏定义用于定义常量和简化代码。使用 #define 可以创建宏,宏可以是常量,也可以是函数式宏。
#include <stdio.h>
// 定义一个常量宏
#define PI 3.14
// 定义一个函数式宏
#define SQUARE(x) ((x) * (x))
int main() {
printf("PI: %f\n", PI); // 输出: PI: 3.140000
printf("Square of 5: %d\n", SQUARE(5)); // 输出: Square of 5: 25
return 0;
}
2. 文件包含 (#include)
文件包含用于将其他文件的内容插入到当前文件中,通常用于引入头文件。可以使用尖括号 < > 或双引号 "" 来包含文件。
尖括号 < >:通常用于引入系统头文件或标准库文件。
双引号 " ":通常用于引入用户自定义的头文件。编译器首先在当前源文件所在的目录中查找文件,如果未找到,再去系统标准目录查找。
#include <stdio.h> // 系统头文件
#include "myheader.h" // 用户自定义头文件
int main() {
printf("Hello, World!\n");
return 0;
}
myheader.h 示例内容:
// myheader.h
void greet() {
printf("Greetings from the header file!\n");
}
在主文件中调用 greet() 函数:
greet(); // 输出: Greetings from the header file!
3. 条件编译 (#ifdef, #ifndef, #if, #else, #endif)
条件编译用于根据条件选择性地编译代码,可以用来控制代码的编译,通常用于调试或在不同平台上进行特定编译。
#include <stdio.h>
// 定义一个宏
#define DEBUG
int main() {
#ifdef DEBUG
printf("Debug mode is ON\n");
#else
printf("Debug mode is OFF\n");
#endif
return 0;
}