🌱 一些记录? 🌱

快捷导航

计算机系统概述

1686558508376

  • 用低/高电平分别表示 0/1

  • 计算机硬件唯一能识别的数据: 二进制 0/1

  • 通过电信号传递数据

计算机发展历程

1686558584827


  • 计算机系统 = 硬件 + 软件

    • 硬件: 计算机的实体,如主机、外设等
    • 软件: 由具有各类特殊功能的程序组成
  • 计算机性能的好坏取决于“软”、“硬”件功能的总和

1686895322828

1686895296561

  • 计算机的机器字长是指数据运算的基本单位长度

计算机硬件的基本组成

1686558136285


冯诺依曼结构

1686557745503

  • “存储程序”的概念是指将指令以二进制代码的形式事先输入计算机的主存储器,然后按其在存储器中的首地址执行程序的第一条指令,以后就按该程序的规定顺序执行其他指令,直至程序执行结束

  • 在计算机系统中,软件和硬件在逻辑上是等效的。

  • Eg:对于乘法运算,可以设计一个专门的硬件电路实现乘法运算也可以用软件的方式,执行多次加法运算来实现

1686896055606


现代计算机结构

1686558042815

  • 现代计算机:以存储器为中心
  • CPU=运算器+控制器

1686558073967

1686558095585


各硬件部件工作原理

1686558889130


主存储器的基本组成

1686896512542


运算器的基本组成

1686896670266


控制器的基本组成

1686896830119

  • 完成一条指令:
  • 取指
    • 取指令 PC
    • 分析指令 IR
  • 执行
    • 执行指令 CU

计算机的工作过程

1686559163405

1686559212840

1686559791291

  • 执行指令阶段: 不同的指令具体步骤不同
  • cpu 区分指令和数据的依据: 指令周期的不同阶段
  • 现在的计算机通常把 MAR 、 MDR 也集成在 CPU 内

计算机系统的多级层次结构

1686576914817


计算机的层次结构

1686576816780

  • 下层是上层的基础,上层是下层的扩展

计算机体系结构 vs 计算机组成原理

1686577069791


计算机软件

1686577705723


系统软件和应用软件

1686577355612


三种级别的语言

1686578282761

  • 编译器 、 汇编器 、 解释器 , 可统称 “ 翻译程序 ”

软件和硬件的逻辑功能等价性

1686898144248

  • 同一个功能 , 既可以用硬件实现 ( 性能高成本高 ) , 也可以用软件实现 ( 性能低成本也低 )
  • 指令集体系结构(ISA):软件和硬件之间的界面。设计计算机系统的ISA,就是要定义一台计算机可以支持哪些指令,以及每条指令的作用是什么、每条指令的用法是什么。

计算机系统的工作原理

从 c 语言源程序到可执行文件

1686578515195


计算机的性能指标

1686579581892


  • 描述频率 、 速率时: K -> M-> G -> T -> P -> E -> Z (10^3递增, K= 10^3)

存储器的性能指标

1686578733712

1686578834606


CPU的性能指标

  • CPU 主频 : CPU 内数字脉冲信号振荡的频率

1686579093508

1686579204703


系统整体的性能指标

1686579362131

  • 动态测试

  • 基准程序是用来测量计算机处理速度的一种实用程序,以便于被测量的计算机性能可以与运行相同程序的其它计算机性能进行比较。

  • 主频高的CPU一定比主频低的CPU快吗?

    • 不一定,如两个CPU,A的主频为2GHz,平均CPI=10;B的主频1GHz,平均CPI=1…
  • 若A、B两个CPU的平均CPI相同,那么A一定更快吗?

    • 也不一定,还要看指令系统,如 A不支持乘法指令,只能用多次加法实现乘法;而B支持乘法指令。
  • 基准程序执行得越快说明机器性能越好吗?

    • 基准程序中的语句存在频度差异,运行结果也不能完全说明问题

数据的表示和运算

进位计数制

1686904209556


r进制计数法

1686902402672


任意进制 -> 十进制

1686902725559


二进制 <-> 八进制十进制

1686902942677


各种进制常见书写方式

1686903042892


十进制 -> 任意进制

整数部分: 除基取余

1686903699707

1686903649106

小数部分: 乘基取整

1686903361906

1686903369287


十进制 -> 二进制 (拼凑法)

1686903872169


真值和机器数

1686904085924


定点数的表示

1686906910624


无符号数的定点表示

1686904632714

  • 通常只有无符号整数, 没有无符号小数

有符号数的定点表示

1686904835109


原码

1686905016342

1686905238959


反码

1686905388439


补码

1686907794431


移码

1686906092144

1686906231112

1686906313234


练习

1686906613718




数据的机器级表示

补码

  • 有符号数才称补码,无符号数没有补码
  • 对于有符号数,最高位为1,就是负数,最高位为0,就是正数
  • 补码是对原码取反后加1的结果
  • 补码变原码依然是取反加1
  • 计算机的 CPU 无法做减法操作(硬件上没有减法器),只能做加法操作
  • 计算机所做的减法,都是通过加法器将其变化为加法实现的
  • 由于是X86架构是小端存储,小端存储是低字节在前,高字节在后
  • 即低字节在低地址,高字节在高地址,大端和小端则相反
  • 注意,通过8位表示, -5的补码是 1111 1011 , -5的原码是 1000 0101 ,符号位是不动的,只有值的部分是 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main() {
int i,j,k;
//5的原码 : 0000 0000 0000 0000 0000 0000 0000 0101

//-5的补码 : 1111 1111 1111 1111 1111 1111 1111 1011
//-5在内存中存储为: 0x ff ff ff fb (小端存储: fb ff ff ff)

//2 + (-5)
//2的原码 : 0000 0000 0000 0000 0000 0000 0000 0010
//-5的补码 : 1111 1111 1111 1111 1111 1111 1111 1011
//2 + (-5) : 1111 1111 1111 1111 1111 1111 1111 1101
//2 + (-5) : -3

i=-5;
j=2;
k=j+i;
printf("k=%d\n",k);
return 0;
}
1
k=-3

反码

  • 反码是一种在计算机中数的机器码表示
  • 正数的反码和原码一样
  • 负数的反码就是在原码的基础上符号位保持不变,其它位取反
十进制 原码 反码
6 0000 0110 0000 0110
-3 1000 0011 1111 1100

整形溢出解析

  • 不同整型变量表示的整型数的范围不同,超出范围会发生溢出现象,导致计算出错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main() {
int i = 10;
short a = 32767;
short b = 0;
b = a + 1;//发生了溢出,解决溢出的办法是用更大的空间来存
printf("%d\n", b);//b不是32767
printf("\n");
//a=32767 : 0111 1111 1111 1111
// 1 : 0000 0000 0000 0001
//a + 1 : 1000 0000 0000 0000 = -32768

unsigned short m = 0x8056;//无符号类型,最高位不认为是符号位
short n = 0x8056;
printf("m=%u\n", m);//无符号类型要用%u,用%d是不规范的
printf("n=%d\n", n);//n是有符号类型,所以输出是负值
//unsigned short m = 0x8056 : 1000 0000 0101 0110 32854
//short n = 0x8056 : 1000 0000 0101 0110 -32682
return 0;
}
1
2
3
4
-32768

m=32854
n=-32682

浮点数 IEEE754 标准

float
符号位 指数部分 小数部分
0 8 23
double
符号位 指数部分 小数部分
0 11 52
  • 浮点型数据是按照指数形式存储的
  • 系统把一个浮点型数据分成小数部分(用 M 表示)和指数部分(用 E 表示)并分别存放。
  • 指数部分采用规范化的指数形式,指数也分正、负(符号位,用 S 表示)

例如:

格式 SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
二进制数 01000000 10010000 00000000 00000000
十六进制数 40 90 00 00
  • S:S 是符号位,用来表示正、负,是 1 时代表负数,是 0 时代表正数
  • E:E 代表指数部分
    • 指数部分的值规定只能是 1 到 254,不能是全 0,全 1
    • 指数部分运算前都要减去 127(这是 IEEE-754 的规定),因为还要表示负指数
    • 这里的 10000001 转换为十进制数为 129,129 − 127 = 2,即实际指数部分为 2
  • M:M 代表小数部分
    • 这里为 0010 0000 0000 0000 0000 000
    • 底数左边省略存储了一个1(这是 IEEE-754 的规定),使用的实际底数表示为 1.0010 0000 0000 0000 0000 000

等价于

S 阶码 尾数
0 10000001 0010 0000 0000 0000 0000 000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

int main() {
float f = 4.5;
//4.5 的内存: 00 00 90 40
//实际: 0x 40 90 00 00
//换成二进制: 0100 0000 1001 0000 0000 0000
//符号位: 0
//阶码: 100 0000 1 十进制为129 129-127=2 2^2=4
//尾数: 001 0000 0000 0000
//实际底数: 1.001 0000 0000 0000 十进制为1.125
//结果: 4*1.125=4.5
//结果:指数部分为2, 1.001左移2位, 100.1十进制为4.5

float f1 = 1.456;
//1.456 的内存: 35 5e ba 3f
//实际值: 0x 3f ba 5e 35
//换成二进制: 0011 1111 1011 1010 0101 1110 0011 0101
//符号位: 0
//指数部分: 011 1111 1 十进制为127 2^0=1
//小数部分: 011 1010 0101 1110 0011 0101
//小数部分: 1.011 1010 0101 1110 0011 0101 十进制为1.456

printf("f = %f\n", f);
printf("f1 = %f\n", f1);
return 0;
}
1
2
f = 4.500000
f1 = 1.456000

浮点数精度丢失

  • 针对强制类型转换,int转float可能造成精度丢失,因为 int 是有 10 位有效数字的
  • int转为double不会造成精度丢失,float转为double也不会丢失精度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main() {
//赋值的一瞬间发生精度丢失
float a = 1.23456789e10; //12345678900
float b;
b = a + 20;//计算时精度丢失,不为 12345678920
printf("b=%f\n", b);

double m = 1.23456789e10; //12345678900
double n;
n = m + 20;
printf("n=%lf\n", n);
return 0;
}
1
2
b=12345678848.000000
n=12345678920.000000

汇编语言

汇编指令格式

  • CPU 是如何执行我们的程序的?

    • 我们编译后的可执行程序, 也就是 main.exe, 是放在代码段
    • 读取了代码段的某一条指令后, 会交给译码器来解析
    • 这时候译码器就知道要做什么事情了
    • CPU 中的计算单元加法器不能直接对栈上的某个变量, 直接做加 1 操作的
    • 需要首先将栈, 也就是内存上的数据, 加载到寄存器中
    • 然后再用加法器做加 1 操作, 再从寄存器搬到内存上去
  • CPU 读写寄存器的速度比读写内存的速度要快很多!

  • 指令地址是由 程序计数器(PC)给出, PC存放当前欲执行指令的地址, 而指令的地址码字段则保存的操作数的地址


  • 操作码字段:表征指令的操作特性与功能(指令的唯一标识), 不同的指令操作码不能相同

  • 地址码字段:指定参与操作的操作数的地址码

  • 指令中指定操作数存储位置的字段称为地址码, 地址码中可以包含存储器地址, 也可包含寄存器编号


  • 指令中可以有一个两个或者三个操作数, 也可没有操作数

  • 根据一条指令有几个操作数地址, 可将指令分为零地址指令一地址指令二地址指令三地址指令四地址指令

  • 零地址指令: 只有操作码, 没有地址码(空操作 停止等)

  • 一地址指令: 指令编码中只有一个地址码, 指出了参加操作的一个操作数的存储位置, 如果还有另一个操作数则隐含在累加器中

  • 二地址指令: 指令编码中有两个地址, 分别指出了参加操作的两个操作数的存储位置, 结果存储在其中一个地址中

  • 二地址指令格式中, 从操作数的物理位置来说有可归为三种类型

    • 寄存器-寄存器(RR)型指令: 需要多个通用寄存器或个别专用寄存器, 从寄存器中取操作数, 把操作结果放入另一个寄存器, 机器执行寄存器-寄存器型的指令非常快, 不需要访存。

    • 寄存器-存储器(RS)型指令: 执行此类指令时, 既要访问内存单元, 又要访问寄存器。

    • 存储器-存储器(SS)型指令: 操作时都是涉及内存单元, 参与操作的数都是放在内存里, 从内存某单元中取操作数, 操作结果存放至内存另一单元中, 因此机器执行指令需要多次访问内存。

  • 三地址指令: 指令编码中有三个地址码, 指出了参加操作的两个操作数的存储位置和一个结果的地址


  • 复杂指令集: 变长 x86 CISC Complex Instruction Set Computer

  • 精简指令集: 等长 arm RISC Reduced Instruction Set Computin

生成汇编的方法

  • 编译过程
    • main.c —> 编译器 —> main.s 文件(.s 文件就是汇编文件, 文件内是汇编代码)
    • main.s 汇编文件—> 汇编器—> main.obj
    • main.obj 文件—> 链接器—> 可执行文件
1
gcc -S -fverbose-asm main.c
1
gcc -m32 -masm=intel -S -fverbose-asm main.c

相关寄存器

  • EBP 堆栈基指针 (Base Pomter)
  • ESP 堆栈顶指针 (Stack Pomter)
  • EBPESP 外, 其他几个寄存器的用途是比较任意的, 也就是什么都可以存。

汇编常用指令

  • 汇编指令通常可以分为数据传送指令逻辑计算指令控制流指令

  • 下面以 Intel 格式为例, 介绍一些重要的指令

  • 以下用于操作数的标记分别表示寄存器、内存和常数。

  • <reg>: 表示任意寄存器, 若其后带有数字, 则指定其位数

    • <reg32>表示 32 位寄存器(eax、ebx、ecx、edx、esi、edi、esp 或 ebp)
    • <reg16>表示 16 位寄存器(ax、bx、cx 或 dx)
    • <reg8>表示 8 位寄存器(ah、al、bh、bl、ch、cl、dh、dl)
  • <mem>: 表示内存地址(如 [eax][var+4]dword ptr [eax+ebx])

  • <con>: 表示 8 位、16 位或 32 位常数

    • <con8>表示 8 位常数
    • <con16>表示 16 位常数
    • <con32>表示 32 位常数。(也称为立即数)

数据传送指令

  • mov 指令: 将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第一个操作数(寄存器或内存)。但不能用于直接从内存复制到内存。

    1
    mov eax ebx # 将 ebx 值复制到 eax
  • push 指令: 将操作数压入内存的栈, 常用于函数调用。ESP 是栈顶, 压栈前先将ESP 值减 4(栈增长方向与内存地址增长方向相反), 然后将操作数压入 ESP 指示的地址。

    • 注意, 栈中元素固定为 32 位
  • pop 指令: 与 push 指令相反, pop 指令执行的是出栈工作, 出栈前先将 ESP 指示的地址中的内容出栈, 然后将 ESP 值加 4。

算术和逻辑运算指令

  • add/sub 指令: add 指令将两个操作数相加, 相加的结果保存到第一个操作数中。sub指令用于两个操作数相减, 相减的结果保存到第一个操作数中。

    1
    sub exa 10 # eax <--- eax-10
  • inc/dec 指令: inc、dec 指令分别表示将操作数自加 1、自减 1。

  • imul 指令: 带符号整数乘法指令, 有两种格式:

    • 两个操作数, 将两个操作数相乘, 将结果保存在第一个操作数中, 第一个操作数必须为寄存器
    • 三个操作数, 将第二个和第三个操作数相乘, 将结果保存在第一个操作数中, 第一个操作数必须为寄存器
    • 乘法操作结果可能溢出, 则编译器置溢出标志 OF = 1, 以使 CPU 调出溢出异常处理程序。
  • idiv 指令: 带符号整数除法指令, 它只有一个操作数, 即除数, 而被除数则为 edx:eax中的内容(64 位整数), 操作结果有两部分: 商和余数, 商送到 eax, 余数则送到edx。

  • and/or/xor 指令。and、or、xor 指令分别是按位与按位或按位异或操作指令, 用于操作数的位操作(按位与, 按位或, 异或), 操作结果放在第一个操作数中

  • not 指令: 位翻转指令, 将操作数中的每一位翻转, 即 0→1、1→0。

  • neg 指令: 取负指令。

  • shl/shr 指令: 逻辑移位指令, shl 为逻辑左移, shr 为逻辑右移, 第一个操作数表示被操作数, 第二个操作数指示移位的位数。

  • lea 指令: 地址传送指令, 将有效地址传送到指定的的寄存器。

控制流指令

  • 控制PC指针去哪里
  • x86 处理器维持着一个指示当前执行指令的指令指针(IP),
  • 当一条指令执行后, 此指针自动指向下一条指令
  • IP 寄存器不能直接操作, 但可以用控制流指令更新
  • 通常用标签(label)指示程序中的指令地址, 在 x86 汇编代码中, 可在任何指令前加入标签

  • jmp 指令: jmp 指令控制 IP 转移到 label 所指示的地址(从 label 中取出指令执行)。

  • jcondition 指令: 条件转移指令, 依据 CPU 状态字中的一系列条件状态转移。CPU状态字中包括指示最后一个算术运算结果是否为 0, 运算结果是否为负数等

    • je <label> (jump when equal)
    • jne <label> (jump when not equal)
    • jz <label> (jump when last result was zero)
    • jg <label> (jump when greater than)
    • jge <label> (jump when greater than or equal to)
    • jl <label> (jump when less than)
    • jle <label> (jump when less than or equal to)
  • cmp/test 指令: cmp 指令用于比较两个操作数的值, test 指令对两个操作数进行逐位与运算, 这两类指令都不保存操作结果, 仅根据运算结果设置 CPU 状态字中的条件码。

  • call/ret 指令: 分别用于实现子程序(过程、函数等)的调用及返回。

    • call 指令首先将当前执行指令地址入栈, 然后无条件转移到由标签指示的指令
    • 与其他简单的跳转指令不同, call 指令保存调用之前的地址信息(当 call 指令结束后, 返回调用之前的地址)
    • ret 指令实现子程序的返回机制, ret 指令弹出栈中保存的指令地址, 然后无条件转移到保存的指令地址执行
    • call 和 ret 是程序(函数)调用中最关键的两条指令。

条件码

  • 编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。

  • 除了整数寄存器, CPU 还维护着一组条件码(标志位)寄存器, 它们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令

  • CF: 进(借)位标志。最近无符号整数加(减)运算后的进(借)位情况有进(借)位, CF=1;否则 CF=0。如 (unsigned) t < (unsigned) a , 因为判断大小是相减

  • ZF: 零标志。最近的操作的运算结算是否为 0。若结果为 0, ZF=1;否则 ZF=0

  • SF: 符号标志。最近的带符号数运算结果的符号负数时, SF=1;否则 SF=0

  • OF: 溢出标志。最近带符号数运算的结果是否溢出, 若溢出, OF=1;否则 OF=0

  • OF 和 SF 对无符号数运算来说没有意义, 而 CF 对带符号数运算来说没有意义


  • 如何判断溢出, 简单的就是正数相加变负数为溢出, 负数相加变正数溢出, 但是考研不这么考, 考研往往给你十六进制的两个数考溢出, 通过如下手法判断即可。

    • 数据高位进位, 符号位未进位, 溢出。
    • 数据位高位未进位, 符号位进位, 溢出。
    • 数据位高位进位, 符号位进位, 不溢出。
    • 数据位高位未进位, 符号位未进位, 不溢出。
  • 数据位高位和符号位高位进位不一样的时候会溢出

  • 常见的算术逻辑运算指令(add、sub、imul、or、and、shl、inc、dec、not、sal 等)会设置条件码

  • 但有两类指令只设置条件码而不改变任何其他寄存器, 即 cmp 和 test 指令

  • cmp指令和 sub 指令的行为一样, test 指令与 and 指令的行为一样, 但它们只设置条件码, 而不更新目的寄存器

  • 控制流指令中的 Jcondition 条件转移指令, 就是根据条件码 ZF 和 SF 来实现转跳。

  • 乘法溢出后, 可以跳转到溢出自陷指令

  • 例如 int 0x2e 就是一条自陷指令, 但是考研只需要掌握溢出, 可以跳转到溢出自陷指令即可, 不需要记自陷指令有哪些。

变量赋值汇编

  • 整型, 整型数组, 整型指针变量的赋值(浮点与字符等价的)

  • 我们的 C 代码在让 CPU 去运行时, 其实所有的变量名都已经消失了, 实际是数据从一个空间, 拿到另一个空间的过程

  • 我们访问所有变量的空间都是通过栈指针(esp)时刻都存着栈指针, 也可以称为栈顶指针的偏移, 来获取对应变量内存空间的数据的。

  • ptr – pointer (指针)的缩写

  • 汇编里面 ptr 是规定的字 (保留字), 是用来临时指定类型的

  • 可以理解为, ptr 是临时的类型转换, 相当于 C 语言中的强制类型转换

  • intel 中的

    • dword ptr 长字节(4 字节)
    • word ptr 双字节
    • byte ptr 一字节
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int arr[3] = {1, 2, 3};
int *p;
int i = 5;
int j = 10;
i = arr[2];
p = arr;
printf("i=%d\n", i);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
	.file	"main.c"
.intel_syntax noprefix
# GNU C17 (Rev10, Built by MSYS2 project) version 12.2.0 (x86_64-w64-mingw32)
# compiled by GNU C version 12.2.0, GMP version 6.2.1, MPFR version 4.2.0, MPC version 1.3.1, isl version isl-0.25-GMP

# GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
# options passed: -m32 -masm=intel -mtune=generic -march=nocona
.text
.def _printf; .scl 3; .type 32; .endef
_printf:
push ebp #
mov ebp, esp #,
push ebx #
sub esp, 36 #,
# D:/Programs/msys64/mingw64/include/stdio.h:371: __builtin_va_list __local_argv; __builtin_va_start( __local_argv, __format );
lea eax, [ebp+12] # tmp86,
mov DWORD PTR [ebp-16], eax # __local_argv, tmp86
# D:/Programs/msys64/mingw64/include/stdio.h:372: __retval = __mingw_vfprintf( stdout, __format, __local_argv );
mov ebx, DWORD PTR [ebp-16] # __local_argv.0_1, __local_argv
mov DWORD PTR [esp], 1 #,
mov eax, DWORD PTR __imp____acrt_iob_func # tmp87,
call eax # tmp87
mov DWORD PTR [esp+8], ebx #, __local_argv.0_1
mov edx, DWORD PTR [ebp+8] # tmp88, __format
mov DWORD PTR [esp+4], edx #, tmp88
mov DWORD PTR [esp], eax #, _2
call ___mingw_vfprintf #
mov DWORD PTR [ebp-12], eax # __retval, tmp89
# D:/Programs/msys64/mingw64/include/stdio.h:374: return __retval;
mov eax, DWORD PTR [ebp-12] # _10, __retval
# D:/Programs/msys64/mingw64/include/stdio.h:375: }
mov ebx, DWORD PTR [ebp-4] #,
leave
ret
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "i=%d\12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
push ebp #
mov ebp, esp #,
and esp, -16 #,
sub esp, 48 #,
# main.c:5: int main() {
call ___main #
# main.c:6: int arr[3] = {1, 2, 3};
mov DWORD PTR [esp+24], 1 # arr[0],
mov DWORD PTR [esp+28], 2 # arr[1],
mov DWORD PTR [esp+32], 3 # arr[2],
# main.c:8: int i = 5;
mov DWORD PTR [esp+44], 5 # i,
# main.c:9: int j = 10;
mov DWORD PTR [esp+40], 10 # j,
# main.c:10: i = arr[2];
mov eax, DWORD PTR [esp+32] # tmp84, arr[2]
mov DWORD PTR [esp+44], eax # i, tmp84
# main.c:11: p = arr;
lea eax, [esp+24] # tmp85,
mov DWORD PTR [esp+36], eax # p, tmp85
# main.c:12: printf("i=%d\n", i);
mov eax, DWORD PTR [esp+44] # tmp86, i
mov DWORD PTR [esp+4], eax #, tmp86
mov DWORD PTR [esp], OFFSET FLAT:LC0 #,
call _printf #
# main.c:13: return 0;
mov eax, 0 # _10,
# main.c:14: }
leave
ret
.ident "GCC: (Rev10, Built by MSYS2 project) 12.2.0"
.def ___mingw_vfprintf; .scl 2; .type 32; .endef

选择循环汇编

  • 字符串常量是存在文字常量区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main()
{
int i=5;
int j=10;
if (i < j)
{
printf("i is small\n");
}
for(i=0;i<5;i++)
{
printf("this is loop\n");
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
	.file	"main.c"
.intel_syntax noprefix
# GNU C17 (Rev10, Built by MSYS2 project) version 12.2.0 (x86_64-w64-mingw32)
# compiled by GNU C version 12.2.0, GMP version 6.2.1, MPFR version 4.2.0, MPC version 1.3.1, isl version isl-0.25-GMP

# GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
# options passed: -m32 -masm=intel -mtune=generic -march=nocona
.text
.def _printf; .scl 3; .type 32; .endef
_printf:
push ebp #
mov ebp, esp #,
push ebx #
sub esp, 36 #,
# D:/Programs/msys64/mingw64/include/stdio.h:371: __builtin_va_list __local_argv; __builtin_va_start( __local_argv, __format );
lea eax, [ebp+12] # tmp86,
mov DWORD PTR [ebp-16], eax # __local_argv, tmp86
# D:/Programs/msys64/mingw64/include/stdio.h:372: __retval = __mingw_vfprintf( stdout, __format, __local_argv );
mov ebx, DWORD PTR [ebp-16] # __local_argv.0_1, __local_argv
mov DWORD PTR [esp], 1 #,
mov eax, DWORD PTR __imp____acrt_iob_func # tmp87,
call eax # tmp87
mov DWORD PTR [esp+8], ebx #, __local_argv.0_1
mov edx, DWORD PTR [ebp+8] # tmp88, __format
mov DWORD PTR [esp+4], edx #, tmp88
mov DWORD PTR [esp], eax #, _2
call ___mingw_vfprintf #
mov DWORD PTR [ebp-12], eax # __retval, tmp89
# D:/Programs/msys64/mingw64/include/stdio.h:374: return __retval;
mov eax, DWORD PTR [ebp-12] # _10, __retval
# D:/Programs/msys64/mingw64/include/stdio.h:375: }
mov ebx, DWORD PTR [ebp-4] #,
leave
ret
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "i is small\12\0"
LC1:
.ascii "this is loop\12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
push ebp #
mov ebp, esp #,
and esp, -16 #,
sub esp, 32 #,
# main.c:5: {
call ___main #
# main.c:6: int i=5;
mov DWORD PTR [esp+28], 5 # i,
# main.c:7: int j=10;
mov DWORD PTR [esp+24], 10 # j,
# main.c:8: if (i < j)
mov eax, DWORD PTR [esp+28] # tmp84, i
cmp eax, DWORD PTR [esp+24] # tmp84, j
jge L4 #,
# main.c:10: printf("i is small\n");
mov DWORD PTR [esp], OFFSET FLAT:LC0 #,
call _printf #
L4:
# main.c:12: for(i=0;i<5;i++)
mov DWORD PTR [esp+28], 0 # i,
# main.c:12: for(i=0;i<5;i++)
jmp L5 #
L6:
# main.c:14: printf("this is loop\n");
mov DWORD PTR [esp], OFFSET FLAT:LC1 #,
call _printf #
# main.c:12: for(i=0;i<5;i++)
add DWORD PTR [esp+28], 1 # i,
L5:
# main.c:12: for(i=0;i<5;i++)
cmp DWORD PTR [esp+28], 4 # i,
jle L6 #,
# main.c:16: return 0;
mov eax, 0 # _9,
# main.c:17: }
leave
ret
.ident "GCC: (Rev10, Built by MSYS2 project) 12.2.0"
.def ___mingw_vfprintf; .scl 2; .type 32; .endef

如何得到机器码

1
gcc -m32 -g -o main main.c
1
objdump --source main.exe >main.dump

函数调用汇编

  • 先必须明确的一点是, 函数栈向下生长的

  • 向下生长, 是指从内存高地址向低地址的路径延伸

  • 栈有栈底和栈顶, 栈顶的地址要比栈底的低

  • 对 x86 体系的 CPU 而言, 寄存器ebp可称为帧指针基址指针(base pointer), 寄存器esp可称为栈指针(stack pointer)

  • ebp 在未改变之前始终指向栈帧的开始(也就是栈底), 所以 ebp 的用途是在堆栈中寻址

  • esp 会随着数据的入栈和出栈而移动, 即 esp 始终指向栈顶


  • call _add 做了什么(感觉不到)

    1
    `call _add`下一条指令的地址压栈
  • _add: push ebp做了什么(感觉不到)

    1
    `esp`指向`add`函数的`ebp`
  • ret 做了什么(感觉不到)

    • 把 ebp 内的内容复制到 esp 寄存器中, 也就是 B 函数的栈基作为原有调用者A 函数的栈顶
    • 弹出栈顶元素, 放到 ebp 寄存器中, 因为原有 A 函数的栈基指针压到了内存里, 所以弹出后, 放入 ebp, 这样原函数 A 的现场恢复完毕
    • pop ebp返回地址main的ebp都弹栈出去
      1
      2
      mov esp,ebp
      pop ebp

  • 下面的.dump文件中的e8代表call, 而ab ff ff ff是通过00401510减去401565所得
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int add(int a, int b) {
int ret;
ret = a + b;
return ret;
}
//指针的间接访问, 函数调用, 返回值
int main() {
int a, b, ret;
int *p;
a = 5;
p = &a;
b = *p + 2;
ret = add(a, b);
printf("add result=%d\n", ret);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
	.file	"main.c"
.intel_syntax noprefix
# GNU C17 (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) version 8.1.0 (x86_64-w64-mingw32)
# compiled by GNU C version 8.1.0, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.18-GMP

# GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
# options passed: -imultilib 32
# -iprefix C:/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/8.1.0/
# -D_REENTRANT main.c -m32 -masm=intel -mtune=generic -march=i686
# -fverbose-asm
# options enabled: -faggressive-loop-optimizations
# -fasynchronous-unwind-tables -fauto-inc-dec -fchkp-check-incomplete-type
# -fchkp-check-read -fchkp-check-write -fchkp-instrument-calls
# -fchkp-narrow-bounds -fchkp-optimize -fchkp-store-bounds
# -fchkp-use-static-bounds -fchkp-use-static-const-bounds
# -fchkp-use-wrappers -fcommon -fdelete-null-pointer-checks
# -fdwarf2-cfi-asm -fearly-inlining -feliminate-unused-debug-types
# -ffp-int-builtin-inexact -ffunction-cse -fgcse-lm -fgnu-runtime
# -fgnu-unique -fident -finline-atomics -fira-hoist-pressure
# -fira-share-save-slots -fira-share-spill-slots -fivopts
# -fkeep-inline-dllexport -fkeep-static-consts -fleading-underscore
# -flifetime-dse -flto-odr-type-merging -fmath-errno -fmerge-debug-strings
# -fpeephole -fplt -fprefetch-loop-arrays -freg-struct-return
# -fsched-critical-path-heuristic -fsched-dep-count-heuristic
# -fsched-group-heuristic -fsched-interblock -fsched-last-insn-heuristic
# -fsched-rank-heuristic -fsched-spec -fsched-spec-insn-heuristic
# -fsched-stalled-insns-dep -fschedule-fusion -fsemantic-interposition
# -fset-stack-executable -fshow-column -fshrink-wrap-separate
# -fsigned-zeros -fsplit-ivs-in-unroller -fssa-backprop -fstdarg-opt
# -fstrict-volatile-bitfields -fsync-libcalls -ftrapping-math
# -ftree-cselim -ftree-forwprop -ftree-loop-if-convert -ftree-loop-im
# -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
# -ftree-phiprop -ftree-reassoc -ftree-scev-cprop -funit-at-a-time
# -funwind-tables -fverbose-asm -fzero-initialized-in-bss -m32 -m80387
# -m96bit-long-double -maccumulate-outgoing-args -malign-double
# -malign-stringops -mavx256-split-unaligned-load
# -mavx256-split-unaligned-store -mfancy-math-387 -mfp-ret-in-387
# -mieee-fp -mlong-double-80 -mms-bitfields -mno-red-zone -mno-sse4
# -mpush-args -msahf -mstack-arg-probe -mstv -mvzeroupper

.text
.globl _add
.def _add; .scl 2; .type 32; .endef
_add:
push ebp #
mov ebp, esp #,
sub esp, 16 #,
# main.c:5: ret = a + b;
mov edx, DWORD PTR [ebp+8] # tmp93, a
mov eax, DWORD PTR [ebp+12] # tmp94, b
add eax, edx # tmp92, tmp93
mov DWORD PTR [ebp-4], eax # ret, tmp92
# main.c:6: return ret;
mov eax, DWORD PTR [ebp-4] # _4, ret
# main.c:7: }
leave
ret
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "add result=%d\12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
push ebp #
mov ebp, esp #,
and esp, -16 #,
sub esp, 32 #,
# main.c:9: int main() {
call ___main #
# main.c:12: a = 5;
mov DWORD PTR [esp+16], 5 # a,
# main.c:13: p = &a;
lea eax, [esp+16] # tmp91,
mov DWORD PTR [esp+28], eax # p, tmp91
# main.c:14: b = *p + 2;
mov eax, DWORD PTR [esp+28] # tmp92, p
mov eax, DWORD PTR [eax] # _1, *p_5
# main.c:14: b = *p + 2;
add eax, 2 # tmp93,
mov DWORD PTR [esp+24], eax # b, tmp93
# main.c:15: ret = add(a, b);
mov eax, DWORD PTR [esp+16] # a.0_2, a
mov edx, DWORD PTR [esp+24] # tmp94, b
mov DWORD PTR [esp+4], edx #, tmp94
mov DWORD PTR [esp], eax #, a.0_2
call _add #
mov DWORD PTR [esp+20], eax # ret, tmp95
# main.c:16: printf("add result=%d\n", ret);
mov eax, DWORD PTR [esp+20] # tmp96, ret
mov DWORD PTR [esp+4], eax #, tmp96
mov DWORD PTR [esp], OFFSET FLAT:LC0 #,
call _printf #
# main.c:17: return 0;
mov eax, 0 # _10,
# main.c:18: }
leave
ret
.ident "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def _printf; .scl 2; .type 32; .endef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>

int add(int a, int b) {
40150f: 90 nop

00401510 <_add>:
401510: 55 push %ebp
401511: 89 e5 mov %esp,%ebp
401513: 83 ec 10 sub $0x10,%esp
int ret;
ret = a + b;
401516: 8b 55 08 mov 0x8(%ebp),%edx
401519: 8b 45 0c mov 0xc(%ebp),%eax
40151c: 01 d0 add %edx,%eax
40151e: 89 45 fc mov %eax,-0x4(%ebp)
return ret;
401521: 8b 45 fc mov -0x4(%ebp),%eax
}
401524: c9 leave
401525: c3 ret

00401526 <_main>:
//指针的间接访问, 函数调用, 返回值
int main() {
401526: 55 push %ebp
401527: 89 e5 mov %esp,%ebp
401529: 83 e4 f0 and $0xfffffff0,%esp
40152c: 83 ec 20 sub $0x20,%esp
40152f: e8 ec 00 00 00 call 401620 <___main>
int a, b, ret;
int *p;
a = 5;
401534: c7 44 24 10 05 00 00 movl $0x5,0x10(%esp)
40153b: 00
p = &a;
40153c: 8d 44 24 10 lea 0x10(%esp),%eax
401540: 89 44 24 1c mov %eax,0x1c(%esp)
b = *p + 2;
401544: 8b 44 24 1c mov 0x1c(%esp),%eax
401548: 8b 00 mov (%eax),%eax
40154a: 83 c0 02 add $0x2,%eax
40154d: 89 44 24 18 mov %eax,0x18(%esp)
ret = add(a, b);
401551: 8b 44 24 10 mov 0x10(%esp),%eax
401555: 8b 54 24 18 mov 0x18(%esp),%edx
401559: 89 54 24 04 mov %edx,0x4(%esp)
40155d: 89 04 24 mov %eax,(%esp)
401560: e8 ab ff ff ff call 401510 <_add>
401565: 89 44 24 14 mov %eax,0x14(%esp)
printf("add result=%d\n", ret);
401569: 8b 44 24 14 mov 0x14(%esp),%eax
40156d: 89 44 24 04 mov %eax,0x4(%esp)
401571: c7 04 24 00 40 40 00 movl $0x404000,(%esp)
401578: e8 bf 0f 00 00 call 40253c <_printf>
return 0;
40157d: b8 00 00 00 00 mov $0x0,%eax
401582: c9 leave
401583: c3 ret
401584: 66 90 xchg %ax,%ax
401586: 66 90 xchg %ax,%ax
401588: 66 90 xchg %ax,%ax
40158a: 66 90 xchg %ax,%ax
40158c: 66 90 xchg %ax,%ax
40158e: 66 90 xchg %ax,%ax

文件的操作

1
2
3
4
5
6
7
8
9
10
11
struct _iobuf {
char *_ptr; //下一个要被读取的字符地址
int _cnt; //剩余的字符, 若是输入缓冲区, 则表示缓冲区中还有多少个字符未被读取
char *_base; //缓冲区基地址
int _flag; //读写状态标志位
int _file; //文件描述符
int _charbuf;
int _bufsiz; //缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;
  • Windows 操作系统下的 FILE 结构体与 Linux 操作系统, Mac 操作系统下的 FILE 结构体中的成员变量名是不一致的, 但是其原理可以互相参考

C 文件概述

  • 程序执行时就称为进程, 进程运行过程中的数据均在内存中

  • 需要存储运算后的数据时, 就需要使用文件。这样程序下次启动后, 就可以直接从文件中读取数据

  • 文件是指存储在外部介质(如磁盘、磁带)上的数据集合, 操作系统是以文件为单位对数据进行管理的

  • 缓冲文件系统: 系统自动地在内存区为每个正在使用的文件开辟一个缓冲区。用缓冲文件系统进行的输入/输出称为高级磁盘输入/输出。

  • 非缓冲文件系统: 系统不自动开辟确定大小的缓冲区, 而由程序为每个文件设定缓冲区。用非缓冲文件系统进行的输入/输出称为低级输入/输出。

  • 缓冲区其实就是一段内存空间, 分为读缓冲写缓冲。C 语言缓冲的三种特性如下:

    • 全缓冲: 在这种情况下, 当填满标准 I/O 缓存后才进行实际 I/O 操作。全缓冲的典型代表是对磁盘文件的读写操作。

    • 行缓冲: 在这种情况下, 当在输入和输出中遇到换行符时, 将执行真正的 I/O 操作。这时, 我们输入的字符先存放到缓冲区中, 等按下回车键换行时才进行实际的 I/O 操作。典型代表是标准输入缓冲区(stdin)和标准输出缓冲区(stdout)。

    • 不带缓冲: 也就是不进行缓冲, 标准出错情况(stderr)是典型代表, 这使得出错信息可以直接尽快地显示出来

  • 输出文件缓冲区和输入文件缓冲区是同一个缓冲区

  • 标准输入缓冲区(stdin)和标准输出缓冲区(stdout)不是

文件打开及关闭

  • fopen 函数用于打开由 fname(文件名)指定的文件, 并返回一个关联该文件的流
    • r 打开一个用于读取的文本文件
    • w 创建一个用于写入的文本文件, 如果存在会清空文件
    • a 附加到一个文本文件,文件存在不会清空文件
    • rb 打开一个用于读取的二进制文件
    • wb 创建一个用于写入的二进制文件
    • ab 附加到一个二进制文件
    • r+ 打开一个用于读/写的文本文件
    • w+ 创建一个用于读/写的文本文件
    • a+ 打开一个用于读/写的文本文件
    • rb+ 打开一个用于读/写的二进制文件
    • wb+ 创建一个用于读/写的二进制文件
    • ab+ 打开一个用于读/写的二进制文件
  • fclose 函数用于关闭给出的文件流, 并释放已关联到流的所有缓冲区
  • fputc 函数用于将字符 ch 的值输出到 fp 指向的文件中
  • fgetc 函数用于从指定的文件中读入一个字符, 该文件必须是以读或读写方式打开的
  • perror 函数是读取错误码来分析失败原因的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

//file.txt content
// ciallociallociallo

//练习文件打开
int main() {
FILE *fp;//定义一个FILE类型的指针变量
fp=fopen("file.txt","r+");//打开文件
if(NULL==fp) {//判断文件是否打开失败了
perror("fopen");//perror帮忙定位失败原因
return -1;
}
char c;
// c=fgetc(fp);
// printf("%c\n",c);
// c=fgetc(fp);
// printf("%c\n",c);
while((c=fgetc(fp))!=EOF) {//读取文件内的所有内容
printf("%c",c);
}
printf("\n");
c=fputc('H',fp);
if(-1==c) {
perror("fputc");
return -1;
}
fclose(fp);//关闭文件
return 0;
}
1
ciallociallociallo

文件读写

  • fread函数与 fwrite函数
    • fread 函数的返回值是读取的内容数量
    • fwrite 写成功后的返回值是已写对象的数量
1
2
int fread(void *buffer, size_t size, size_t num, FILE *stream);
int fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
  • fread 和 fwrite 函数既可以以文本方式对文件进行读写, 又可以以二进制方式对文件进行读写

  • fgets 函数与 fputs 函数
    • 函数 fgets 从给出的文件流中读取[num-1]个字符, 并且把它们转储到 str(字符串)中。
    • fgets 在到达行末时停止, fgets 成功时返回 str(字符串), 失败时返回 NULL, 读到文件结尾时返回 NULL
    • fputs 函数把 str(字符串)指向的字符写到给出的输出流。成功时返回非负值, 失败时返回 EOF
1
2
char *fgets(char *str, int num, FILE *stream);
int fputs(const char *str, FILE *stream);
  • 使用 fgets 函数, 我们可以一次读取文件的一行, 这样就可以轻松地统计文件的行数
  • 用于 fgets函数的 buf 不能过小(buf 大于最长行的长度), 否则可能无法读取”\n”, 导致行数统计出错
  • fputs 函数向文件中写一个字符串, 不会额外写入一个”\n”
  • fgetc fputc fgets fputs 只能读取字符串

  • windows下(在 Mac 和 Linux 操作系统下并不存在这样的问题)以文本方式下写入”\n”后, 磁盘存储的是”\r\n”, 当然读取”\r\n”时底层接口会自动转换为”\n”。而以二进制方式写入”\n”后, 磁盘存储的是”\n”,二者在其他方面没有差异

  • 写入整型数, 浮点数时, 一定要用二进制方式时, 需要以”rb+”方式打开文件

  • 二进制方式下内存中存储的是什么, 写入文件的就是什么, 是一致的

  • 如果是以文本方式写入的内容, 那么一定要以文本方式读取

  • 如果是以二进制方式写入的内容, 那么一定要以二进制方式读取, 不能混用!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <string.h>

int main() {
char buf[20]="ciallo\nyoshino";
FILE *fp;
int ret;//存储函数的返回值
//r+代表以文本方式打开文件
fp=fopen("file.txt","rb+");
if(NULL==fp) {
perror("fopen");
return -1;
}
// ret=fwrite(buf,sizeof(char),strlen(buf),fp);//把buf中的字符串写入文件

//file.txt content
// ciallo
// yoshino

char buf1[20]={0};
ret=fread(buf1,sizeof(char),sizeof(buf1),fp);
printf("%s\n",buf1);
fclose(fp);
return 0;
}
1
2
ciallo
yoshino

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main() {
FILE* fp;
int i=123456;
int ret;
fp=fopen("file.txt","wb+");
if(NULL==fp) {
perror("fopen");
return -1;
}
//向文件中写入整型数, 如果我们双击打开文件会发现乱码, 因为打开文件都是以字符格式去解析的
// ret=fwrite(&i,sizeof(int),1,fp);

//file.txt content
// 二进制的, 存储int: 123456, 打开乱码

i=0;
fread(&i,sizeof(int),1,fp);
printf("i=%d",i);
fclose(fp);
return 0;
}
1
i=0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

//file.txt content
// ciallo
// yoshino
// ciallo

int main() {
char buf[20]={0};//用于存储读取数据
FILE* fp;
fp=fopen("file.txt","r+");//可读可写打开文件
if(NULL==fp) {
perror("fopen");
return -1;
}
//一次读一行, 读空文件
while(fgets(buf,sizeof(buf),fp)!=NULL) {//fgets读取到文件结束时返回NULL
printf("%s",buf);
}
return 0;
}
1
2
3
ciallo
yoshino
ciallo

文件位置指针偏移

  • fseek 函数的功能是改变文件的位置指针

    • fseek(文件类型指针,位移量,起始点)
    • 位移量是指以起始点为基点, 向前移动的字节数。一般要求为 long 型
    • fseek 函数调用成功时返回零, 调用失败时返回非零
  • 起始点的说明如下:

    • 文件开头 SEEK_SET 0
    • 文件当前位置 SEEK_CUR 1
    • 文件末尾 SEEK_END 2
1
int fseek(FILE *stream, long offset, int origin);
  • ftell 函数返回 stream(流)当前的文件位置, 发生错误时返回-1
    • 当我们想知道位置指针距离文件开头的位置时, 就需要用到 ftell 函数
1
long ftell(FILE *stream);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <string.h>

int main() {
FILE *fp;
char str[20]="ciallo\nyoshino";
int len=0;//用于保存字符串长度
long pos;
int ret;//接函数返回值
fp =fopen("file.txt","w+");//打开文件
if(NULL==fp) {
perror("fopen");
return -1;
}
len=strlen(str);
fwrite(str,sizeof(char),len,fp);
ret=fseek(fp,-7,SEEK_CUR);
if(ret!=0) {
perror("fseek");
fclose(fp);
return -1;
}
pos= ftell(fp);
printf("now pos=%ld\n",pos);// 根据实际写入内容统计(\r\n)
memset(str,0,sizeof(str));//清空str
fread(str,sizeof(char),sizeof(str),fp);//读取
printf("str=%s\n",str);
return 0;
}
1
2
now pos=8
str=yoshino