逆向-c语言基础

c语言基础

vc6++或者VisualStudio 并结合反汇编窗口
vs改版,添加了很多_s的安全函数
Visual Studio 2019使用scanf提示错误c4996解决方法

https://docs.microsoft.com/zh-cn/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4996?f1url=%3FappId%3DDev16IDEF1%26l%3DZH-CN%26k%3Dk(C4996)%26rd%3Dtrue&view=msvc-160

可直接参考菜鸟教程

https://www.runoob.com/cprogramming/c-tutorial.html
函数:一系列指令的集合,为了重复使用调用
函数类型 函数名(参数列表)

{ //函数体
	return;
}

重点
(1)函数类型、函数名,不可省略,参数可以省略(不必要/不重要的参数)
(2)函数名、参数名,只能以字母、数字、下划线组成,且第一个字母必须是字母或者下划线
(3)区分大小写,不可使用关键字

什么是参数?参数如何传递?什么是返回值?返回值放在哪?
堆栈传参,传参顺序从右往左
堆栈图

变量

变量,就是一个存储数据的容器
变量名就是内存地址的一个别名
变量类型确定内存宽度
声明变量
变量类型 变量名;
变量类型和函数类型相似,用来说明存储数据的宽度是多大。
int 整数型 4字节
char 字节型 1字节
变量命名规则
1、只能以字母、数字、下划线组成,第一位必须是字母或者下划线
2、区分大小写
3、不可使用关键字

全局变量特点:
1、编译的时候就确定好了内存地址和宽度,变量名就是内存地址别名
2、不重写编译,全局变量内存地址不变
3、全局变量中的值,任何程序都可以修改,是公用的

局部变量
1、局部变量是函数内部申请的,如果函数没执行,局部变量就没有内存空间
2、局部变量的内存是从堆栈中分配的,程序执行才分配。由于我们无法预知程序什么时候执行,也就意味我们无法确定局部变量的内存地址。
3、因局部变量的内存地址无法确定,所以,局部变量只能在函数内使用,无法在函数外使用。

函数嵌套调用

在函数里面可以定义变量,可以做运算,可以写任何代码,也可以调用其他函数。
堆栈缓冲区

数据类型

常见的整数数据类型
char 字节型 1字节 0~FF
int 整数型 2/4 字节 0~FFFF / 0~FFFFFFFF
short 短整型 2字节 0~FFFF
long 长整型 4字节 0~FFFFFFFF
(int在十六位计算机中是2字节,比如在TC老编译器中
在32以上的计算机中是4字节,比如在VC新编译器中)
有无符号数(signed、unsigned)
无论是有无符号数,编译器都是按照反码原码和补码的方式存储的

浮点类型
常见浮点类型如下:
float 单精度浮点型 4字节
double 双精度浮点型 8 字节
float和double存储方式上都遵守IEEE编码规范。

字符与字符串
数据类型决定了宽度能存储多少,数据存储的格式
我们使用符号时,存到内存中的并不是符号本身,而是符号对应的一个编号
ASCLL码表
putchar函数 getchar函数
ASCLL码表中一共有127个字符,也就是最大7F,1个字节就够用,所以我们尽量用char来存储,不要用int等浪费不必要的空间
中文字符
两个大于127的组合到一起,代表一个中文,可以存储大约7000多个简体汉字
GB2312、GB2312-80、Unicode
两个字节代表一个中文。
所有的编码,字符,都是查表即可

运算符与表达式

表达式是由一系列运算符(operators)和操作数(operands)组成的

      • / == = 一系列符号就是运算符
        常见运算符
  • 加法 a+b
  • 减法 a-b
  • 乘法 a*b
    / 除法 a/b
    % 取余 a%b
    ++ 自加 a++ / ++a
    – 自减 a-- / --a
    a++ 是先使用再+1,++a 是先+1,再使用,–a 和 a–同理。

分支语句

1.if else语句
单if语句

if(表达式)
{
	语句;
}

if、else语句

if(表达式)
{
	语句1;
}
else
{
	语句2;
}

if、else if、else语句

if(表达式1)
{
	语句1;
}
else if(表达式2)
{
	语句2;
}
else if(表达式3)
{
	语句3;
}
else
{
	都没判断到的语句;
}

2.if 嵌套语句

if(表达式)
{
	if(表达式)
	{
		执行语句;
	}
}
else
{
	if(表达式)
	{
		执行语句;
	}
}

嵌套实例

#include <stdio.h>
void main()
{
	//假设你要结婚了,女方家长问了你三个问题
	int money; //你有多少存款啊?
	int horse; //你有多少房子啊?
	int old; //你多大了?
	printf("你有多少存款啊?  \n");
	scanf("%d",&money);  //用于用户输入,也就好像你丈母娘问你,你要回答他问题,他把这信息记到了心里。
	printf("你有多少房子啊?  \n");
	scanf("%d",&horse);
	printf("你多大了?  \n");
	scanf("%d",&old);
	if (money >= 100000)  //判断有没有十万
	{
		if (horse<2)  //如果够了十万判断是否有两套以上的房子
		{
			printf("有十万又怎么样,房子没两套就是穷\n");  //没有就执行
		}
		else   //如果有
		{
			if (old<=25)  //再问你多大了,是不是25以下
			{
				printf("嗯,25之前有十万,又有两套房,也算是年少有为了,嫁给你吧 \n");  //嗯,有十万,又有房,又25一下,满足条件,迎娶美人
			}
			else
			{
				printf("都那么大年纪了,才挣够,我再考虑下。\n");  //年龄超过了预算,有也不考虑你
			}
		}
	}
	else
	{
		printf("有没有搞错?十万都没有 \n");  //嗯,没有十万块钱,直接pass
	}
}

if语句在汇编层面实现
可利用反汇编查看

4.switch case语句

switch(表达式)
{
	case 常量1:   //如果达成条件1
		语句;
		break;  //跳出
	case 常量2:
		语句;
		break;
	default:   //如果所有case都不满足
		语句;
		break;  //跳出
}

注意:
1、表达式结果不可是浮点数
2、case后的常量值不能一样,且必须为常量。
3、注意case 常量后是":"冒号
4、一定不要忘了break,如果没有break就会一直往下执行,一直看到break为止
5、default语句不分位置,就是放到case上面,也不会影响执行顺序
条件合并写法(若case1和case2我要执行的语句是一样的)

switch(表达式)
{
	case 常量1:case 常量2:  //两个case合并到了一起
		语句;
		break; 
	case 常量3:
		语句;
		break;
	default:
		语句;
		break;
}

switch和if的区别
1、switch只进行等值判断(直接一个值判断),if、else可区间判断(比如a>b、b>c)
2、switch执行效率高于if、else语句,分支越多越明显。

if、else是在不停的判断跳转,switch通过一个jmp可以去任何地方

5.while 循环语句

while (条件表达式)
	{
		执行语句;
	}

while语句嵌套

break 跳出离着这个break最近的switch和while
continue 直接返回到离着最近的while或者switch,下面的语句全不执行

九九乘法表(while嵌套while)

#include <stdio.h>
void main()
{
	int a = 0;
	while(a<10)
	{
		int b = 1;
		while(b<=a)
		{
			printf("%d * %d = %d   ",a,b,a*b);
			b++;
		}
		printf("\n");
		a++;
	}
	return;
}

6.do while 循环语句

#include <stdio.h>
void main()
{
	do 
	{
		//执行代码
	} while (/*判断表达式*/);  //while判断条件
	return;
}

while和do while的区别:
1、while会判断成立后才执行
2、do while,会先执行一次代码,再去判断是否成立,也就是不管成立不成立,都先执行一次
7.for 循环语句

for (1、表达式1;2、表达式2;3、表达式3)
	{
		//4、执行代码
	}

利用反汇编理解以上分支语句的原理及执行

数组

数组定义格式
数据类型 变量名[常量];
中括号里的常量,要写定义多少个变量;数组就是一堆变量声明到了一起。
不给初始化数值的话,会使用堆栈默认填充的值:CC
初始化的两种方式
int age[5] = {1,2,3,4,5};
int age[] = {1,2,3,4,5};
不管数组后面写不写数量,计算机都会根据后面赋值内容进行检测生成。
数组的读写
多维数组
不管几维数组,在内存中的布局,全都是连续存储。
多维和一维数组本质上是等价的

结构体

定义结构
为了定义结构,您必须使用 struct 语句。struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;struct 类型名{
	int a;
	char b;
	short c;
};

结构体就是相当于帮助我们重新定义了一个新的数据类型。

字节对齐

一个变量占用n个字节,则该变量的起始地址必须是n的整数倍,即存放起始地址%n = 0
如果是结构体,起始地址是最宽数据类型成员的整数倍。
字节对其目的:为了提升程序执行的效率(牺牲空间换效率)
C语言的关键字,sizeof() ,是一个判断数据类型或者表达式长度的运算符
pragma pack(n)它仅仅作用给结构体成员,不会影响结构体!
我们对空间要求较高的时候,就可以通过这种方式改变结构体成员的对齐方式。#pragma pack(n)
它没法作用于结构体,只能作用于结构体成员
pragma pack(n),n代表的是让变量以多少字节对齐方式,可以包括如下值:1、2、4、8,编译器默认是8
当强制取消对齐方式的时候,用 #pragma pack()
每个结构体成员宽度,除了起始位置,都会去和对齐参数作比较,谁小用谁
结构体的总大小 = N的整数倍
N=Min(最大成员,对齐参数)
也就是在最大成员和对齐参数中,取出最小的那个值作为n

结构体数组

定义格式
类型 变量名[常量];
结构体成员的使用
写:结构体数组名[下标].成员名 = 赋值的;
读:接收的变量 = 结构体数组名[下标].成员名;
遍历结构体数组
字符串复制函数strcpy(),
写strcpy(要被拷贝到的位置,被拷贝的内容)
读strcpy_s(接收的变量,复制给变量的内容)
在内存中,结构体数组存储方式。

指针类型

int* a;
short* b;
char* c;
指针赋值
a = (int*)5;
short = (short*)1;
char = (char*)1;
指针类型永远是4字节(dword),不管是什么类型,不管是有多少
++或–时,会加或减,当前"
"星数去掉一个后类型的宽度
非指针的类型,也就是不带的数据类型,++或–,就默认是+1或者-1
指针类型可以做加法、减法,但是不可以做乘除运算
指针变量 + N(要加上的数值) = 指针变量 + N
(去掉号后指针的宽度)
指针变量 + N(要加上的数值) = 指针变量 + N
(去掉*号后指针的宽度)
比较用的jae/jbe,这两个是判断无符号的
指针禁忌
不要去乱自己发挥想象
别什么:指针里面存的是地址。
指针和地址没有很大的关系,他就是一个类型,想存什么存什么,想干什么干什么
只不过有几个特性:
加加 / 减减 与其他不同
有着自己的宽度
可以作比较

&符号的使用

&是地址符,可以用在任何变量上来获取地址,但是不能用在常量上
不可以用在常量上,因为常量不是个容器,是立即数,所以没有内存地址的概念
局部变量地址是不确定的

取值运算符

取值运算符 ""
之前我们认识的
可以做以下几种
乘法运算:ab
定义指针:int
a
取值运算符:* 后面跟一个指针类型的变量
必须与一个指针类型的变量组合,比如(a+1)、(a++),只要后面跟的是指针类型的变量就行
加指针类型变量后 的类型 为原指针类型减去一个

数组参数传递

数组作为参数传递的时候,传递的是数组元素首地址
基本类型作为参数传递的时候,传递的是变量值的副本
可以通过指针来达到遍历数组的效果
数组当作参数传递的时候,起始传递的就是一个地址,我们可以把它当作指针
数组和指针方式传递,我们观察反汇编也没有区别,目的达成效果也相同
数组名自身暗中包含了数组的大小,传递过程中只包含地址,因而丢失了数组大小信息
传递指针:只能改变指针的内容,却不能改变指针本身”
即:
指针本身是一个地址(所指的变量的地址)
形参指针和实参指针就是一起所指的变量的地址。
改变指针内容就是改变 变量 的值。

指针数组 / 数组指针

定义:
char arr[10]; //就是一个数组,有10个成员,每个成员宽度为1字节,因为是char类型
char* arr[10]; //指针数组,长度10,名为arr,每个成员为char*指针类型,4字节
赋值
如:

int main()
{
	char* a = "Hello";  
	//编译器会在常量区分一块内存,存储Hello的ASCII码
	//然后把内存的地址存到a,所以a和b里存的都是一个地址,因为是char* 所以都是4字节
	char* b = "Cracker";
}

指针数组本质就是数组,只不过里面存的是各种各样的指针
具体链接

https://www.cnblogs.com/hongcha717/archive/2010/10/24/1859780.html

调用约定

函数调用约定:就是编译器三件事:怎么传递参数、返回值,如何平衡堆栈
如果我们没有声明调用约定
默认使用__cdecl调用方式,从右到左入栈,调用者(也就是调用函数的地方,比如再main函数里调用的这个函数,main函数就称为调用者)清堆栈
__stdcall调用方式,从右到左入栈,自身清理堆栈
__fastcall,ECX/EDX传送前两个,剩下的从右到左入栈,自身清理堆栈
__fastcall的效率比stdcall和cdecl效率要高,因为从CPU中读数据比内存中读数据要快
stdcall基本用在windows提供的api上
平时写代码基本都用cdecl
深入了解参考微软提供帮助

函数指针

定义:
返回类型 (调用约定 *变量名)(参数列表);

int (__cdecl *CeShi)(int a,int b);

函数指针无法做加加或者减减,因为加加减减需要砍掉一颗星,砍掉一颗星后就是一普通函数,无法确定宽度,加多少减多少

预处理

预处理:源代码转为二进制代码之前,由预处理器对程序源代码进行处理,处理后再由编译器进一步编译
预处理的功能主要包括:宏定义、文件包含、条件编译
宏定义
//简单格式 #define 标识符 字符序列
复杂宏定义
常见宏定义
常见宏定义

多次调用,重复使用,用函数,节省软件体积,简单调用,用宏定义,可以节省空间
总结
函数和宏定义的区别:
1、宏定义不会浪费空间,比较节省
2、函数比宏定义编译出来的程序体积要小。
什么时候用函数和宏?
1、简单的,可以使用宏
2、很复杂的,就用函数完成
文件包含

实操很重要,加深理解和编程能力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值