dustland

dustball in dustland

CSAPP-chapter3 x86-64 Assembly

汇编变种后缀的应用场景

前置知识:

0.C语言数据格式

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main() {
cout << "sizeof(char)=" << sizeof(char) << endl;
cout << "sizeof(short)=" << sizeof(short) << endl;
cout << "sizeof(int)=" << sizeof(int) << endl;
cout << "sizeof(long)=" << sizeof(long) << endl;
cout << "sizeof(long long)=" << sizeof(long long) << endl;
cout << "sizeof(void*)=" << sizeof(void *) << endl;
}

在64位ubuntu上的运行结果

1
2
3
4
5
6
7
8
root@deutschball-virtual-machine:/home/deutschball/mydir# g++ test.cpp -o test.out
root@deutschball-virtual-machine:/home/deutschball/mydir# ./test.out
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=8
sizeof(long long)=8
sizeof(void*)=8

在64位windows上的运行结果稍有不同

1
2
3
4
5
6
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=4
sizeof(long long)=8
sizeof(void*)=8

在32位windows上的运行结果

1
2
3
4
5
6
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(long)=4
sizeof(long long)=8
sizeof(void*)=4
操作系统(字节) char short int long long long void*
linux64位 1 2 4 8 8 8
windows64位 1 2 4 4 8 8
windows32位 1 2 4 4 8 4

1.寄存器规格

大多数的后缀都与寄存器规格有关,下图将会被多次用到

image-20220406175644150

2.寻址方式

image-20220406180238442

mov类

操作数长度相关后缀b,w,l,q

mov类命令的数据流动方向有五种,

1
2
3
4
5
立即数->寄存器
寄存器->寄存器
主存 ->寄存器
立即数->主存
寄存器->主存

显然立即数->立即数是不可能的,这里立即数相当于右值,就好比说把5存到6上

并且规定不能从主存直接到主存即主存->主存,必须经过寄存器

数据长度有关的后缀

image-20220406174739043

这里两个字节等于一个字

便于记忆,可以了解后缀的含义:

b:byte,一个字节

w:word,一个字(两个字节)

使用哪种后缀需要与数据流动方向一起决定,具体规则是:

1
2
3
4
5
6
7
1.主存没有决定后缀的权利

2.如果源或者目的其一是寄存器则根据寄存器大小决定,如果使用的是`%al,%spl`这种单字节寄存器则mov命令用b后缀,
同理如果使 `%rax,%rsp`这种8个字节寄存器则mov命令用q后缀.
如果源和目的都是寄存器则跟随目的寄存器规格

3.寄存器比立即字的优先级高

记住这三条规则才能完成3.2

image-20220406175452026

1.movl %eax,(%rsp)

image-20220406175737319

%eax为32位(=4字节=2字)寄存器,

目的(%rsp)是采用简介寻址,实际地址位于内存中,没有发言权,

因此mov的后缀跟随%eav即传送双字,使用l后缀

2.movw (%rax),%dx

image-20220406180020141

(%rax)在内存中,没有发言权,

目的%dx是一个16位(=2字节=1字)寄存器

因此mov的后缀跟随%dx即传送单字,使用w后缀

3.movb $0xFF %bl

image-20220406180630017

$0xFF是一个立即字,优先级低

目的%bl是一个字节寄存器,优先级高

mov后缀跟随%bl使用b

4.movb (%rsp,%rdx,4) %dl

image-20220406180836564

(%rsp,%rdx,4)在内存上,没有发言权

目的%dl是一个字节寄存器

mov后缀跟随%dl使用b

5.movq (%rdx),%rax

image-20220406181217813

(%rdx)在内存上,没有发言权

目的%rax是一个四字寄存器

mov后缀跟随%rax使用q

6.movw %dx,(%rax)

image-20220406181231776

%dx是一个单字寄存器

目的(%rax)在内存上,没有发言权

因此mov后缀跟随%dx用w

长度类后缀的其他细节差异
image-20220406191326874

1.movb,movw只会修改目标寄存器的对应低位

2.movl不光会修改目标寄存器的对应低位,并且会将高位全部置零

3.对于64位的立即数,1.只能用movabsq2.将其存到寄存器中,movq只能处理32位的立即数

数据拓展相关后缀z,s

image-20220406193244989

零拓展和符号拓展的区别:

image-20220406194028869

R(%dl)=AA=10101010符号位为1

第4行符号拓展直接将高位全都置1得到一串F

第5行零拓展直接将高位全都置0得到一串0

有符号数拓展时使用符号拓展

无符号数拓展时使用0拓展,可以理解为无符号拓展

image-20220406200228709

1.首先,使用指针的目的是,使实际操作的地址在内存中,如此需要在寄存器中过度一次,将转型分成两个阶段,即源内存->寄存器寄存器->目的内存两个阶段

2.然后一定注意"==当执行强制类型转换即涉及大小变化又涉及C语言中的符号变化时,操作应该先改变大小=="

这里"先改变大小"的意思是,无符号源用z,有符号源用s,然后决定大小的后缀看目的的大小

1.long到long

不涉及大小变化,不涉及符号变化,只需要使用传送四字指令movq,两个阶段相同

2.char到int

只涉及大小变化,首先从字节内存到双字寄存器需要符号拓展指令movsbl 然后从双字寄存器到内存根据寄存器规格决定使用双字传送指令movl

3.char到unsigned

既涉及大小变化,又涉及符号变化

首先改变大小,从有符号字节内存到双字寄存器需要符号拓展指令movsbl

然后从双字寄存器到双字内存根据寄存器规格决定使用双字传送指令movl,即目的符号不起作用

char到unsigned int和char到int形成的汇编语言是相同的

这一点可以实验验证

对于char的最小值-128=0x80,如果强制转型到unsigned,可能的结果:

1.首先变化符号,然后变化大小,即首先使用movzbl,然后movl,这样unsigned的值为0x00000080=128

2.首先变化大小,然后变化符号,即首先使用movsbl,然后movl,这样unsigned值为0xFFFFFF80=4294967168

基于上述两种猜想,可以写如下程序验证

image-20220406201752965

证明猜想2是正确的

4.unsigned char到long

既涉及大小变化,又涉及符号变化

首先改变大小,从无符号字节内存到四字寄存器,要使用无符号拓展指令movzbq

然后从四字寄存器到内存,使用四字传送指令movq

然而实际上首先使用的是movzbl

查阅了知乎

然后官方给出的解释:

(Clarification, not an erratum) Figure 3.5.

Although there is an instruction movzbq, the GCC compiler typically generates the instruction movzbl for this purpose, relying on the property that an instruction generating a 4-byte with a register as destination will fill the upper 4 bytes of the register with zeros.

尽管应该使用movzbq指令,但是GCC编译器通常使用movzbl指令来达到相同的目的,

==这是因为只要是以寄存器为目的并且生成低位4字节的指令都会将高位的4字节置零==

博客上其他人的解释

image-20220406203013260

这就很明白了

5.int到char

只涉及大小转换,大变小直接截取

直接从内存中取出双字数据放到寄存器里然后截取低8位传送给内存

即首先使用movl然后movb

6.unsigned到unsigned char

只涉及大小转换,大变小直接截取

直接从内存中取出双字数据放到双字寄存器然后截取低8位传送给内存

即首先使用movl然后movb

7.char到short

只涉及大小变换,小变大需要拓展

首先使用有符号拓展movsbw将字节数据传送到单字寄存器

然后使用movw从单字寄存器传送到内存

汇编语言特殊算术操作

首先纠错

image-20220407195514558

书上除法这里是有错误的,商和余数都放在了R[%rdx]这显然是不合理的,我觉得应该写为:

image-20220407210130248

乘法

c源文件

1
2
3
4
5
6
#include <inttypes.h>
typedef unsigned __int128 uint128_t;
//此处改名的目的是让128位整数能够类似64位整数使用
void store_uprod(uint128_t *dest,uint64_t x,uint64_t y){
*dest=x*(uint128_t) y;
}

汇编语言

1
2
3
4
5
movq    %rsi, %rax
mulq %rdx
movq %rax, (%rdi)
movq %rdx, 8(%rdi)
ret

逻辑分析:

首先三个参数分别存放在

image-20220407195244614

1
2
3
R[%rdi]=dest//这里%rdi寄存器中存放的是dest这个位置 dest这个位置 dest这个位置
R[%rsi]=x
R[%rdx]=y

1.movq %rsi, %rax

即令R[%rax]=R[%rsi]=x将x放在了%rax寄存器中

2.mulq %rdx

R[%rdx]=y

image-20220407195537665

看似只有一个操作数,实际上隐含着另一个操作数在%rax寄存器中

即令R[%rdx]*R[%rax],两个64位数的计算机结果是一个128位数,显然单独一个64位寄存器是放不下的,因此计算结果被分成两部分

低64位放在%rax寄存器,高64位放在%rdx寄存器

3.movq %rax, (%rdi)

R[%rdi]=dest这是指针指向内存中的位置

M[R[%rdi]]=M[dest]对内存中的位置dest应用解引用函数\(M[dest]\)得到的是该地址上的实际数值

M[R[%rdi]]=R[%rax],将%rax寄存器中刚刚算出的低64位结果放在内存中,位置为R[%rdi](间接寻址)

4.movq %rdx, 8(%rdi)

刚才在第3步时存放了低64位,那么此时应该做到就是存放高64位

M[R(%rdi)+8]=R[%rdx],将%rdx寄存器中刚刚算出的高64位结果放在内存中,位置为R(%rdi)+8(基址+偏移量寻址)

此处的+8为偏移8个字节,因为低地址恰好占用这8个字节

在小端机器上低地址存放低位,高地址存放高位

到此乘法的计算结果已经被分成两个64位数存进了一个uint128_t *指针指向的地址

除法

c源文件

1
2
3
4
5
6
void remdiv(long x,long y,long *qp,long *rp){//希望计算x/y将商存到指针qp指向的地址,将余数存到指针rp指向的地址
long q=x/y;
long r=x%y;
*qp=q;
*rp=r;
}

汇编语言

1
2
3
4
5
6
7
movq    %rdi, %rax
movq %rdx, %r8
cqto
idivq %rsi
movq %rax, (%r8)
movq %rdx, (%rcx)
ret
image-20220407201308477
1
2
3
4
R[%rdi]=x
R[%rsi]=y
R[%rdx]=qp
R[%rcx]=rp

1.movq %rdi, %rax

R[%rax]=R[%rdi]=x将x存放在%rax寄存器里

2.movq %rdx, %r8

R[%r8]=R[%rdx]=qp将商==在内存中的地址==存放在%r8寄存器里

image-20220407210300140

对于一个128位数的除法运算,被除数的低64位存放在%rax寄存器中,高64位存放在%rdx寄存器中

刚才在第1步时已经将64位数x放在%rax寄存器中了,但是高64位所在的%rdx现在被第三个参数qp占用,因此将qp放在另一个寄存器%r8中,然后将%rdx腾出来方便存放高64位

3.cqto

image-20220407205028742

为什么要进行符号拓展?

被除数应该是一个128位数,但是目前我们只是确定了其低64位为x,高64位还是第三个参数的值没有修改,如果此时直接计算则高64位的值可以认为是乱码,那么怎么消除高64位的乱码呢?置零或者置符号,我们将要进行符号除法,因此高64位置符号

即高64位按照R[%rax]=x的符号位拓展

4.idivq %rsi

R[%rsi]=y

R[%rdx]=x mod y

R[%rax]=x/y

5.movq %rax, (%r8)

R[%r8]=qp

M[R[%r8]]=M[qp]=*qp=R[%rax]=x/y

6.movq %rdx, (%rcx)

R[%rcx]=rp

M[R[%rcx]]=M[rp]=*rp=R[%rdx]=x mod y

习题3.12

首先四个参数的存放位置为:

image-20220407201308477

执行除法的时候只会提供一个操作数S作为除数,表示被除数的另两个操作数是隐含的%rax,%rdx

image-20220407210829932

那么在执行除法命令之前,应该把被除数先安置好

1.首先128位被除数的低64位存放在%rax中,即R[%rax]=x 2.然后高64位存放在%rdx中,无符号除法时应当全置0,

但是由于第三个参数qp已经占据了%rdx,因此在将其全都置零之前应当请三个参数挪个地方,比如%r8

movq %rdx,%r8

3.此时就可以将被除数的高64位%rdx寄存器置0了,最直接的置零方法是movq $0,%rdx,还可以利用异或的性质xorq %rdx,%rdx

至于应该选择哪一个?应该选择二进制长度最短的指令

1
2
3
4
0000000000000000 <.text>:
0: 48 c7 c2 00 00 00 00 mov $0x0,%rdx
7: 48 99 cqto
9: 48 31 d2 xor %rdx,%rdx

由此可见为什么刚才有符号除法时要用cqto,因为其长度最短

然后xor也是不错的选择

最迫不得已才会选择movq指令

当发现实际编译器使用的命令与我们理想的不一样时,可以写一个.s文件然后将自己理想的汇编指令和实际的汇编指令各写一行

然后使用gcc -Og -c命令,使其编译成为.o文件,注意必须指定-c选项,否则直接编译成.exe或者.out会发生链接错误,因为刚才我门写的.s文件是非常不完整的,连main函数都没有

然后对.o文件使用objdump命令反编译 ,就可以观察指令及其二进制编码长度了

一般理想与现实不同都是由于有更加短但是可以完成同样目的的指令我们没有考虑到

在本题中我不知道类似cqto但是是无符号拓展的指令,可以先用异或指令达到相同的目的

4.被除数在3中已经准备好了,可以进行除法了

image-20220407212434845

这里S是除数,本题中除数是第二个参数y存放在%rsi寄存器中,即R[%rsi]=y

那么除法指令为divq %rsi

5.除法进行完毕,瓜分商和余数

商位于%rax寄存器中,希望传送到内存中的qp位置,而内存中的qp位置存放在寄存器%r8中(即R[%r8]=qp,M[R[%r8]]=*qp)因此使用指令movq %rax,(%r8)

余数位于%rdx寄存器中,希望传送到内存中的rp位置,而内存中的rp位置存放在寄存器%rcx中(即R[%rcx]=rp,M[R[%rcx]]=*rp)因此使用指令movq %rdx,(%rcx)

标志与条件控制

标志位

标志位的作用都是为了表征刚刚进行过的算术运算的结果,比如是否有溢出,是否有进位,结果是否为0等等,

设置这些标志的目的之一是方便判断错误

之二是决定后面的程序走向,是实现条件,==循环等控制语句的基础==

image-20220408152343527

其中常用的标志位有:

image-20220408152408112

作用:

CF

进位标志

作用于无符号数,比如对于八位无符号数

加法结果进位溢出:0xFF+0x01=0x100溢出了,此时CF=1表明刚才的算术运算有溢出

减法借位溢出:0x00-0x01=0x100-0x01=0xFF,被减数需要向他不存在的高位借位,此时CF=1表示被减数不够减的

对于有符号整数的加减法,忽略进位标志CF

比如有符号整数0x00-0x01=0x00+0xFF=0xFF表示的是0-1=-1,

0xFF为-1的补码表示.显然这是合乎情理的

同样的两个数如果是无符号数则计算结果为0xFF=255这两个正数相减越减越多显然不合理,因此CF=1标志有进位溢出

OF

溢出标志

对有符号整数(即补码)运算有效,比如

两个八位二进制数正数01000000(B)=0x40(H)=64相加,

0x40+0x40=0x80(H)=10000000(B)=怎么就成了一个负数-128?

这显然不符合情理,此时OF=1标志有符号数溢出

又或者:

两个八位二进制负数10000000=0x80=-128相加

0x80+0x80=0x100=0x00=0怎么就成了0?

这也是不符合情理的,此时OF=1

怎样检验有符号数加减是否有溢出?

计组上我们学过双标志位法,计算结果的两个符号位如果不同则说明有溢出

OF标志对于无符号整数运算无效

ZF

零标志

如果运算结果为0则ZF=1

SF

符号标志

如果运算结果为负数则SF=1

可以修改标志位的指令

除了leaq外的逻辑运算指令

image-20220408155420102

比较和测试指令

image-20220408155446113

一条指令可能会修改多个标志位,具体规则为:

MOV NOT JMP*

does not affect flags

MOV,NOT,JMP指令不会修改标志位

NEG

The CF flag set to 0 if the source operand is 0; otherwise it is set to 1. The OF, SF, ZF, AF, and PF

flags are set according to the result.

NEG取反指令:如果NEG的操作数是0则CF=1,否则CF=0.

OF,SF,ZF,AF,PF标志根据结果设定

AND OR

The OF and CF flags are cleared; the SF, ZF, and PF flags are set according to the result. The state

of the AF flag is undefined

AND,OR指令的OF和CF前面已经给出;SF,ZF,PF根据结果而定,AF状态无所谓

DEC INC

The CF flag is not affected. The OF, SF, ZF, AF, and PF flags are set according to the result

DEC,INC自增自减指令的CF位不受影响,其他标志位都视结果而定

ADD ADC SUB SBB

The OF, SF, ZF, AF, PF, and CF flags are set according to the result.

ADD,ADC,SUB,SBB指令的所有标志位都视结果而定

CMP

Modify status flags in the same manner as the SUB instruction

CMP与SUB指令对于标志位的效果相同

以两个有符号八位2进制数运算为例子

0x00-0x01=0x00+0xFF=0xFF,OF=0无溢出

0x40+0x40=0x80=10000000=-128,OF=1有溢出

减法也有可能溢出,一个负数减去一个正数时

0x80-0x7F=0x80+0x81=0x101=0x01成了一个正数,此时OF=1

比较指令

image-20220408154303941

执行比较指令后通过==标志位组合==判断比较结果

cmp s1,s2指令基于S2-S1,意思是首先对S2-S1求值,然后结果放在s2原来的地方

如果为负数则SF=1,否则(非负数)SF=0,

后面的程序只需要访问一下Flag Register中的SF值就"可以????"知道S2,S1谁大谁小了

==注意这里"可以"是假的==,

因为如果结果是一个比较大的负数,造成溢出,截断之后成了一个正数,此时SF=0,此时只凭借SF值会造成误判,就比如

0x80-0x7F=0x80+0x81=0x101=0x01结果成了一个正数,那么符号标志位SF=0只凭SF值就会认为0x80>=0x7F

0x80=-128<0x7F=127

因此只凭借SF标志位是无法判断两个操作数的大小的,==还应当考虑是否有溢出==.

==考虑什么时候会发生溢出?==

两个正数相减显然结果绝对值小于两者中的任何一个,不会溢出

两个负数相减同样

两个正数相加显然可以,比如0x7F+0x01=0x80成了负数

两个负数相加也可以,比如0x80+0x80=0x100=0x00成了0

正数-负数也可以,比如0x7F-0xFF=0x7F+0x01=0x80成了负数

负数-正数也可以,比如0x80-0x7F=0x80+0x81=0x101=0x01成了正数

==现在考虑如何完善判断两个数的大小==

两个数做差有四种情况

1.正-正

2.负-负

3.正-负

4.负-正

前两种情况没有溢出,OF=0,直接看SF

后两种可能有溢出,如果没有溢出则看SF,如果有溢出则SF取反

而区分前两种和后两种的方法也很容易观察得出,即两个操作数的符号是否一致

F=[A-B],A为被减数,B为减数,[X]表示对X取符号,X≥0[X]=0,否则[X]=1

列出真值表

image-20220408164041343

我们想要得到F的值,并且我们已经知道OFSF的值,而大小判断与A,B无关,只与Flag Register中的标志位组合有关,那么问题转化为

image-20220408164147132

从真值表上我们可以观察得出,当OF=0时,F和SF是一致的,当OF=1时,F和SF是相反的

那么怎么用一条表达式写出F与OF,SF的关系呢?

根据F=1的项可以立刻得到

\(F=\overline {OF}SF+OF\overline {SF}=OF\oplus SF\)

到此可以总结如何判断两个有符号数的大小了

如果\(OF\oplus SF\)异或值为1则前小后大,否则前大后小

test S1,S2指令基于S1&S2,意思是首先对S1&S2求值,按位与只有两个操作数一模一样才会是真,否则为假.如果为假即0,则ZF=1,否则ZF=0,后面的程序只需要访问一下Flag Register中的ZF值就可以知道S2,S1是否相同了

访问条件码的方式

在刚刚进行完一次逻辑运算之后,此时Flag Register的各个标志位已经设置完毕,下面就要根据其中一个或者几个标志位的组合进行决策,选择执行或者不执行一些命令

1)可以根据条件码的某种组合, 将一个字节设置为0或者1

2)可以条件跳转到程序的某个其他的部分

3)可以有条件地传送数据

1.根据FlagRegister的情况将一个字节设置为0或者1

SET指令

image-20220408202821368

这种指令怎么发挥作用呢?

sete为例子,

假设刚刚进行的运算是a-a=0,此时ZF=1表示刚才的计算结果为0,

此时使用sete 目标寄存器,满足设置条件(相等/零),于是向目标寄存器存放ZF值即1

练习3.13

image-20220408204021860
image-20220408204354747

A项第一行使用了cmpl比较双字命令,说明两个操作数都是双字(int,unsigned),第二行使用setl有符号小于运算,因此操作数只能是int类型

B项第一行使用cmpw比较字命令,说明两个操作数都是字(short,unsigned short),第二行使用setge有符号大于等于,因此操作数只能是short

C项第一行使用了cmpb比较字节命令,说明两个操作数都是字节(char,unsigned char),第二行使用setbe无符号小于等于,因此操作数只能是unsigned char

D项第一行使用了cmpq比较四字命令,说明两个操作数都是四字(long,unsigned long),第二行 使用setne不等/非零,因此操作数无法确定有无符号,long或者unsigned long均可

2.条件跳转

条件跳转表

该表在后来会经常用到

image-20220408155109547
条件跳转指令实现if-else语句

c源代码:

1
2
3
4
5
6
7
8
9
10
long absdiff_se(long x,long y){
long result;
if(x<y){
result=y-x;
}
else {
result=x-y;
}
return result;
}

给定参数x,y如果x<y,则返回y-x,否则返回x-y,即返回两个数的差的绝对值

汇编代码

1
2
3
4
5
6
7
8
9
10
11
absdiff_se:
.LFB0:
movq %rsi, %rax
cmpq %rsi, %rdi
jge .L2
subq %rdi, %rax
ret
.L2:
subq %rsi, %rdi
movq %rdi, %rax
ret

分析其汇编语言都干了什么

首先两个参数的位置

image-20220408154047128

1
2
R[%rdi]=x
R[%rsi]=y

1.movq %rsi, %rax

1
R[%rax]=R[%rsi]=x
image-20220408154223239

将x搬到%rax寄存器中,猜测有可能以后的变化都在x身上进行最后返回%rax寄存器中的值

2.cmpq %rsi, %rdi

注意是==后减前==

做差R[%rdi]-R[%rsi],(一定注意是后减前)SF和OF标志位根据结果确定

3.jge .L2

image-20220408155127559

根据刚才我们推导的R[%rdi]-R[%rsi]<0\(OF\oplus SF=1\),那么R[%rdi]-R[%rsi]≥0就应该有\(!(OF\oplus SF)=1\)

这里jge=jump if greaterR[%rdi]-R[%rsi]≥0的情况

联系前两句,第三句可以理解为:如果\(R[\%rdi]-R[\%rsi]=y-x≥0\)则跳转到.L2

此后程序分叉了,如果3没有满足执行没有跳转则顺序执行subq %rdi, %raxR[%rdi]=R[%rax]-R[%rdi]=x-y

如果3满足条件并且跳转则跳到.L2执行subq %rsi, %rdiR[%rsi]=R[%rdi]-R[%rsi]=y-x一定是非负的

不管是否跳转,3保证了后面的减法一定是大数-小数

汇编语言的程序结构类似于

image-20220408170852710

而c源程序的结构类似于

image-20220408170946086
条件跳转指令实现循环

求一个数n的阶乘,c源文件用while和for分别这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int fact_while(int n){
int ans=1;
int i=1;
while(i<=n){
ans*=i;
++i;
}
return ans;

}
int fact_for(int n){
int ans=1;
for(int i=1;i<=n;++i){
ans*=i;
}
return ans;
}

根据前面学过的标志位的用法,大体可以推测一下汇编语言可能怎么写

参数int n放在edi寄存器

一开始int ans=1;要从一个寄存器里放一个1,由于最后返回的也是ans,因此可以直接用eax(int32位,需要双字寄存器eax,不用四字寄存器rax)寄存器方便返回

然后循环临时变量i也需要一个寄存器存储其值,用edx

循环判断就是i和n的大小判断cmp %edx,%edi,这个式子基于R[%edi]-R[%edx]=n-i

当i<=n时,cmp指令执行完毕之后SF=0恒成立,表示cmp运算结果非负

然后进入循环体ans*=i;,其中R[%eax]=ans,R[%edx]=i,翻译成汇编语言,

image-20220409152257759

imul %eax,%edx计算R[%eax]*R[%edx]然后将结果存放到%eax

循环体执行完毕,下面应该循环变量i自增

image-20220409152802338

inc %edx

下面要回头执行下一次循环,此处应当使用无条件跳转命令,跳到循环判断语句

考虑当循环判断语句不满足时,应当跳转到何处?

使用条件跳转到循环下面的语句

总的来说,推测的汇编逻辑是这样的:

1.安置好形参n,ans,临时变量i在寄存器中的位置

2.循环判断

3.根据刚才的循环判断,决定是否条件跳转出循环

如果没有出循环则执行循环体(乘法),然后自增临时变量i,然后无条件跳转到2

如果跳出了循环则继续执行后面的语句,循环不再考虑

下面实验观察汇编器是如何做的

编译成.o文件之后用objdump反汇编得到

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
Disassembly of section .text:

0000000000000000 <fact_while>:
0: f3 0f 1e fa endbr64
4: b8 01 00 00 00 mov $0x1,%eax
9: ba 01 00 00 00 mov $0x1,%edx
e: 39 f8 cmp %edi,%eax
10: 7f 08 jg 1a <fact_while+0x1a>
12: 0f af d0 imul %eax,%edx
15: 83 c0 01 add $0x1,%eax
18: eb f4 jmp e <fact_while+0xe>
1a: 89 d0 mov %edx,%eax
1c: c3 retq

000000000000001d <fact_for>:
1d: f3 0f 1e fa endbr64
21: b8 01 00 00 00 mov $0x1,%eax
26: ba 01 00 00 00 mov $0x1,%edx
2b: 39 f8 cmp %edi,%eax
2d: 7f 08 jg 37 <fact_for+0x1a>
2f: 0f af d0 imul %eax,%edx
32: 83 c0 01 add $0x1,%eax
35: eb f4 jmp 2b <fact_for+0xe>
37: 89 d0 mov %edx,%eax
39: c3 retq

这样两种循环的写法在机器层面是一模一样的.现在研究其汇编语言的逻辑,以fact_while的汇编语言为例分析其汇编语言的逻辑

1
2
3
4
5
6
7
8
9
10
11
0000000000000000 <fact_while>:
0: f3 0f 1e fa endbr64
4: b8 01 00 00 00 mov $0x1,%eax
9: ba 01 00 00 00 mov $0x1,%edx
e: 39 f8 cmp %edi,%eax
10: 7f 08 jg 1a <fact_while+0x1a>
12: 0f af d0 imul %eax,%edx
15: 83 c0 01 add $0x1,%eax
18: eb f4 jmp e <fact_while+0xe>
1a: 89 d0 mov %edx,%eax
1c: c3 retq

1.mov $0x1,%eax

R[%eax]=0x1

这里我们可能会这样分析:eax寄存器是返回值的一部分,因此可以推断,这一句对应c源代码中的int ans=1;

但是实际上这里对应的是int i=1

==这一点可以从下文中分析得出==

2.mov $0x1,%edx

R[%edx]=0x1,对应的是int ans=1

3.cmp %edi,%eax

这里就可以看出1和2中,或者说%edx,%eax中到底谁放了ans,谁放了i

比较指令,基于R[%eax]-R[%edi],如果与c源程序while(i<=n)对应的话

R[%edi]=n是可以肯定的,因为第一个参数放在edi

那么R[%eax]=i随之确定了

(感觉可以把i和ans的寄存器换一换,这样返回的时候可以直接返回rax,其中正好存放ans,但是当前不知道为啥,编译器没有这样做,而是在最后将edx拷贝到eax)

4.jg 1a <fact_while+0x1a>

jg:jump if greater,

如果!(i<=n)即i>n,则cmp %edi,%edx=R[%edx]-R[%edi]=i-n>0,ZF=0,并且显然这里没有溢出和进位,即满足~(SF^OF)&~SF,jg跳转条件成立,发生条抓

这里和while(i<=n)成立的条件是相同的,都是在进入循环之前首先进行条件判断

注意跳转位置:1a <fact_while+0x1a>

1
2
18:   eb f4                   jmp    e <fact_while+0xe>
1a: 89 d0 mov %edx,%eax

1a位置恰好为无条件跳转的下一句,表明出了循环

5.imul %eax,%edx

==注意后者为目的==

R[%edx]=R[%eax]*R[%edx]

对应循环体ans*=i

6.add $0x1,%eax

R[%eax]=R[%eax]+0x1

对应临时变量i自增

7.jmp e <fact_while+0xe>

无条件跳转,跳转位置是fact_while开始向后偏移0xe字节

image-20220409155811948

实际跳转到判断指令cmp,意思是重新进行循环

8.mov %edx,%eax

如果程序执行到此句,它上一句又是无条件跳转,说明程序是从更早的条件跳转过来的,对应jg 1a <fact_while+0x1a>

这一句的逻辑是把edx寄存器中的东西拷贝给eax一份

edx我们分析过存放的是ans

eax我们分析过存放的是i

然而函数希望返回ans,并且返回值是以eax寄存器为准

因此这里需要把ans搬到eax中

9.retq

函数返回

3.条件传送数据

image-20220409163206775

条件传送数据指令,意思就是当满足某些条件时,才会将数据从哪里拷贝到哪里,还是比较容易理解的

跳转指令

image-20220410230358338

PC相对寻址跳转

首先分析CSAPP给出的例子

image-20220410230546880

1.movq %rdi,%rdx

R[%rdx]=R[%rdi]=x将传入的参数放进rax寄存器

2.jmp .L2

无条件跳转到.L2

.L2:

首先testq %rax,%rax作用是置符号标志,方便判断R[%rax]是负数还是非负数

然后jg .L3 ,jump if greater 即如果R[%rax]>0则跳转.L3

.L3:

sarq %rax,寄存器%rax中的值右移一位,即除以二下取整

显然当R[%rax]=0时.L2中的jg不再满足条件,执行第八行 的返回语句

因此可以推断,源c程序是这样写的:

1
2
3
4
5
void func(int x){
while(x>0){
x>>=1;
}
}

对.o文件使用objdump反汇编得到

image-20220410231054590

第一行和刚才相同

第二行jmp 8这里8怎么来的?左侧的机器码为eb 03,即实际操作数是0x03,然后程序进行到loop+0x3位置即该指令时,首先要做的就是程序计数器移动到下一条指令的位置,即移动loop+0x03这条指令的长度2,此时PC=5,然后jmp 8的8就是PC+0x03=5+3=8,这样得到的

习题3.15

image-20220410231355967

A.当执行4003fa条指令时,PC=4003fc,然后74 02表明要跳转的位置是PC+0x02=0x4003fc+0x02=0x4003fe,即je 0x4003fe

B.当执行40042f条指令时,PC=400431,然后74 f4表明要跳转的位置时PC-0x0C=0x400431-0x0C=0x400425,即je 0x400425

C.执行前一条指令的时候,程序计数器指示下一行的指令位置,即为PC,0x400547=PC+0x02得到PC=0x400545,则第一行的指令地址为0x400545-2=0x400543

D.执行4005e8条指令时,PC=4005ed,字节按照从最低位到最高位的顺序列出为0xff ff ff 73=-141=-0x00 00 00 8D

PC-0x8D=0x4005ed-0x8d=0x400560

因此jmpq 0x400560

习题3.18

image-20220410233232652

程序逻辑分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
R[%rax]=R[%rdi]+R[%rsi]=x+y
R[%rax]=R[%rax]+R[%rdx]=x+y+z
计算R[%rdi]-(-3)=R[%rdi]+3=x+3,根据结果置符号位SF和溢出位OF
如果刚才的值大于等于0即x+3>=0,x>=-3则跳转到.L2
否则计算R[%rsi]-R[%rdx]=y-z
如果刚才的值大于0即y-z=>0即y>=z则跳转.L3
否则R[%rax]=R[%rdi]=z
R[%rax]=R[%rax]*R[%rdi]=x*z;
return R[%rax]=x*z;

.L3:
R[%rax]=R[%rsi]=y
R[%rax]=R[%rax]*R[%rdx]=y*z
return R[%rax]=y*z;

.L2:
计算R[%rdi]-2,g,根据结果置符号位SF和溢出位OF
不考虑溢出,则当SF=1或者ZF=1即刚才的结果小于等于0即R[%rdi]-2<=0即R[%rdi]=x<=2,则跳转.L4
否则R[%rax]=R[%rdi]=x
R[%rax]=R[%rax]*R[%rdx]=x*z

.L4
return R[%rax]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int func(int x,int y,int z){
int temp=x+y+z;
if(x>=-3){
if(x<=2){
return temp;
}
else{
return x*z;
}
}
else if(y>=z){
return y*z;
}
else
return x*y;
}

但是这样写会有4条return语句,答案只有一条return语句,在完成所有条件判断之后返回val

image-20220411082058654

首先改写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int func(int x,int y,int z){
int temp=x+y+z;
if(x>=-3){
if(x<=2){
//啥也不干
}
else{
temp=x*z;
}
}
else if(y>=z){
temp= y*z;
}
else
temp= x*y;
}

然后x>=-3,x<=2下面是没有东西的,考虑不写这一条

1
2
3
4
5
6
7
8
if(x>=-3){
if(x>2)temp=x*z;
}
else if(y>=z){
temp=y*z;
}
else temp=x*y;
return temp;

这样写还是与给定的格式不一样

第一个if下面应该有一个if,有一个else,最后不应该有else

1
2
3
4
5
6
if(x<-3){
if(y>=z)temp=y*z;
else temp=x*y;
}
else if(x>2)temp=x*z;
return temp;

此时就和给定的格式相同了

image-20220411082631746

testq %rsi之后各标志位的情况

R[%rsi] testq %rsi=R[%rsi]+R[%rsi] SF ZF OF CF
正数 无溢出 0 0 0 0
0 无溢出 0 1 0 0
负数 无溢出 1 0 0 0
正数 溢出 1 0 1
负数 溢出 0 0 1

用testq如何判断一个数是正数还是负数?

正数时SF^OF=0andZF=0

负数时SF^OF=1

0时ZF=1

非负数SF^OF=0

那么配合jge

image-20220411085309367

跳转成立条件即\(!(SF\oplus OF)=1\)\(SF\oplus OF =0\)

image-20220411085754492
1
2
3
4
5
6
7
8
9
10
11
12
13
long loop_while2(long a,long b){
if(b<=0){
return b;
}
else{
int temp=b;
while(b>0){
temp*=a;
b-=a;
}
return temp;
}
}
image-20220411090237810
1
2
3
4
5
6
7
8
long loop_while2(long a,long b){
long result=b;
while(b>0){
result=result*a;
b=b-a;
}
return result;
}

递归过程

需要了解一下过程,调用约定

image-20220420205909645
image-20220420205922083

汇编第6行如果跳转实现的话(rdi中存放的x=0)则到11行将被调用者保存的rbx还原,之前又把eax置0,因此返回0,相当于啥也没干

对应rfun函数中的条件判断

1
if(x==0)return 0;

c程序下面的递归过程对应汇编中不满足条件跳转的语句(7到12行)

第7行将rdi中存放的x右移两位

第8行就调用rfun了,rdi是调用者保存的,用来给rfun传递参数,

rfun(x)将会调用rfun(x>>2)

这样一直递归调用到最内层的函数时x==0不满足跳转条件了,返回0,rbx值不变,还是调用者给的值,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
假设上级函数rbx=y
rfun(x)//x放到rdi里
把rbx中的y存一下
x放到rbx里
eax归零
if(x==0){
把刚才放起来y重新放回rbx里
return 0;
}
rdi中的x右移两位成为x1
rfun(x1){}
...
rdi中的x1右移两位成为x2
rfun(x2){
...
}
rbx中的x1加到rax上
返回rax
}
rbx中的x加到rax上
返回rax

rax中累计的是历次x的值

rbx保存"本次"x 的值

rdi也是本次x的值啊?为什么还要rbx保存一下?

在将本次的x累加到rax中之前rdi中的x已经右移两位了,应该用rbx记住x之后再右移

rbx的作用就是记住本次本次本次的x值,

可以这样理解:

如果不用rbx,可以开一个数组,

比如第一层函数的参数x放到数组第一位

第二层函数的参数x1放到数组的第二位

...

然后在返回的时候只需要将下一层函数的返回值与本层在数组中存放的参数值乘起来然后返回

但是实际上寄存器很有限,被调用者寄存器只有六个,如果递归层数很多了则这种数组是不现实的

只用一个的话就是滚动数组的思想了

在返回之前,由于要返回的值已经用完了,rbx的暂时存放使命完成,可以还给上一层函数了,继续承担上一层函数的暂时存放使命

CSAPP-chapter8

感觉大一上学期c语言还有下学期基于c语言的基础课程设计学得好失败.

当时一个位图放缩,我既不懂位图格式,也不懂二进制文件,还不熟悉glibc函数.

本学期操作系统,更是云里雾里,对虚拟内存,进程等概念从未听过,却上来就要讲缺页置换算法,调度算法等等各种算法

对于CSAPP这本书,只能说相见恨晚,应当替代大一下的基础课程设计,作为计组和操作系统先修课.

欠的债在大二下才还上,可以说亡羊补牢?

异常

控制流

一个没有跳转,正常执行的程序,其执行过程中程序计数器PC总是连续变化的,

假设指令序列为 \[ I_1,I_2....,I_n \]

指令对应的地址为 \[ A_1,A_2,...,A_n \] 由于x86上的指令采用变长编码方式,因此\(A_1,A_2,...,A_n\)有可能不是等差数列,但是可以啃腚的是他们连续,

程序计数器PC的值构成的序列被称为控制流,如果程序一直顺序进行没有调用跳转返回,并且没有发生异常,则PC的值一直连续,这样的控制流称为"平滑的"

当程序中有跳转,函数调用,函数返回,或者异常时,控制流不再平滑,此时控制流被称为"异常控制流"(ECF) 异常控制流包括跳转,调用,返回,异常等

异常的定义

先说一些必要的废话

定义:异常就是控制流中的突变,按照这个定义,跳转,调用,返回,异常处理程序都是

异常是异常控制流(ECF)的一种,需要硬件和操作系统协同实现.

异常处理的过程

序幕:异常的触发

异常是如何触发的呢?

处理器中的重大状态变化,状态可以是某些寄存器中的某些位等,被处理器检测到(检测电平的变化对于处理器来说易如反掌,可以实现)

就好像一个人走着走着感觉低血糖了 ,这人自然就感觉到了,然后对低血糖的对策应该吃点糖.这吃点糖就是异常处理程序

吃完糖然后缓过来了继续走路

这就是控制还给之前正在执行的指令

如果低血糖非常厉害,人直接寄了,相当于发生了终止,先前的进程不再进行

开端:异常的察觉与定性

处理器根据状态位的变化判断,发生甚么事了

就好像这个走路的人走着走着感觉头晕,无力,快要饿死,这三种状态一组合,人就知道发生了低血糖

image-20220518201336935

每种事件在制作处理器时都已经交给处理器一个编号,发生某种事件处理器就可以知道对应哪个编号

就好比从小就教给这个人,

高血糖编号0,

低血糖编号1,

尿急编号2,...

然后有一天这个人犯了低血糖,他就知道自己犯了编号为1的毛病

但是这个人知道了自己犯了1号病有啥意义呢?实践生活中也确实没有这样整,应该是怪没有意义的

那么给事件编号的意义又是啥?

这种编号感觉类似协议,处理器调用异常处理程序的时候只需要根据特定的编号去调用特定的处理程序,而处理程序的代码不是处理器管,是操作系统维护的异常表管的,那么处理器用什么信息去查表调用相关的异常处理程序呢?

如果现实中也是这样,人知道了自己犯了1号毛病,去了门诊直接说"我犯了1号病",大夫就知道应该开葡萄糖的药,不需要病人费一大堆话描述"我犯了 头晕无力饿 的病"

同样,处理器在查表之前将自己的各种状态量化成一个魔数,把最困难的事自己解决了,然后用这个魔数去查表岂不是轻轻松松

但是现实生活中并没有给低血糖这种病编号,看起来这种方法很简单快捷,为什么不用?

一是人生活中要记的事情已经太多,病的编号记不住,二是病人觉得是1号病但是大夫可能不这样认为,有可能是更大的毛病

但是对机器来说,没有这么多的可能,根据状态位的变化,已经可以清楚地确定发生了甚么,并且每种毛病的编号都已经被写入硬件,永远牢记,因此可以使用这种方式

发展:根据事件号查异常表

处理器确定事件及事件编号之后,要根据事件编号采取相应的异常处理程序,这时需要去查异常表

异常表是由操作系统启动时分配和初始化的,存放在主存中,事件编号作为下标,异常处理程序的地址作为表项

用事件编号作为下标去查异常表,获得的表项是异常处理程序的地址,然后控制交给该异常处理程序

整个查表的过程与页表的工作方式很像

单级页表的下标是虚拟页号VPN,页表项是物理页号

处理器怎么知道表在哪里的呢?

异常表的起始地址会专门被一个寄存器--异常表基址寄存器(exception table base regsiter)保存,这个寄存器在CPU里

查表的时候

image-20220518203658495

etbr寄存器中存放基址,异常号(可能存放在某个寄存器中)作为偏移量,两个加起来去访问数组项

高潮:异常处理

刚才查完异常表之后处理器已经获得了异常处理程序的首地址,处理器只需要把PC值改为该异常处理程序的首地址

看到这里不禁疑惑,如果异常不是很严重可以被处理,处理完了怎么回到刚才的程序呢?谁来保存刚才进程的执行现场呢?

CSAPP用异常处理和函数调用的对比解决了这个疑惑

执行异常处理程序之前,处理器会将返回时的指令地址,以及其他一些状态(通用寄存器,堆栈指针,程序状态字等等)统统滴压入内核栈,注意不是用户栈

这里从异常处理程序返回时的指令地址只有两种,要么是触发异常的指令地址,要么是下一条指令,

具体是那一条要根据事件类型确定,即根据事件编号确定.

这是后话

为什么要压入内核栈?我的猜测是

异常处理程序要运行在内核状态,访问内核栈方便

为什么异常处理程序要运行在内核状态?我的猜测是

异常处理程序需要请求一些内核的资源,比如缺页异常处理程序会进行磁盘到内存换页的操作,设计了IO操作.而这些在用户状态没有权限做到

上述进程信息都压内核栈保存之后,PC置为异常处理程序的地址,控制交给异常处理程序(或者说异常处理程序占用处理器)

结局:异常处理完了返回

首先考虑一个人大街上走着低血糖了可能怎么处理

一是轻微的,吃块糖缓过来接着走

二是稍微严重,回家歇着了,改天再出来

三是非常严重,人暴毙了(虽然几率不大),这辈子不可能再走路了,也省了处理的麻烦

根据引发异常的事件类型,异常处理完后的返回也可以分成三种

1.返回给当前指令(触发异常的指令),重新进行,比如缺页异常

2.返回给当前指令的下一条指令

下一条指不发生异常时下一条应该执行的指令

3.发生异常的程序被中断,不再执行

异常的类别

异常就四种,中断,陷阱,故障,终止

image-20220518210426662

其中只有属于异步异常的中断发生在CPU之外,只能由IO设备产生,比如按下键盘,点击鼠标,打印到屏幕,等等各种事件

陷阱是有意为之的,目的是从是处理器从用户态陷入内核态(通过修改某种标志位),作用是拥有使用任意资源的权限,比如syscall指令

故障和终止都是不希望发生的,区别是故障算是比较轻微的异常,比如缺页,通过牺牲物理页换入缺页就可以解决

但是终止就是比较严重的异常,程序无法继续运行了,比如栈缓冲区溢出被金丝雀检测到

中断

此处的"中断"更确切的是说"外部中断"或者"硬件中断",即IO外设导致的中断,不是程序有意产生的中断

up九曲阑干举例是键盘输入导致的中断

image-20220518211331500

在这个计算机的模型机中,内存总线和IO总线不是一路,但是汇集到IO桥,然后IO桥通过系统总线连接到CPU中的总线接口,总线接口是与CPU内部寄存器相连的

IO总线上挂着的都是IO外设,比如USB,显示器,键鼠,磁盘等等,计算机就算没有这些东西也可以运行

按下键盘的时候,键盘控制器会向处理器的中断引脚发送中断信号,并且会把异常号通过IO总线,IO桥,系统总线传递给CPU,

中断信号的目的是提醒CPU应该处理中断了,异常号的作用是告诉CPU是键盘出现异常,而不是鼠标

中断信号和异常号都是CPU判断异常事件需要用到的信息

由于键盘发送信号和CPU处理指令是异步的,因此当键盘发送中断信号的时候,CPU有可能还在执行一条命令.CPU必须完成了手头上那一条命令之后,才可以处理中断.

完成手头上的指令后,CPU检测到中断引脚那里发生了电位变化,因此知道发生中断了,然后通过异常号知道是哪个IO设备发生了中断,根据这两个信息CPU就能确定发生了什么事,下面就可以异常处理了

异常处理完之后,应该返回什么地方呢?由于刚才手头上的最后一条指令已经被彻底执行了,自然应该返回下一条指令.

这就好比

正在写作业,突然有人敲门,为了防止当前正在解决的数学问题思路被打断重来,我先做完这道题,然后去处理访客的问题

到了门口得先问问是谁在敲门,是熟人就开门

然后得端茶倒水儿,把客人招待好,等客人走了把门一关回去接着做下一道题

这里数学题就相当于CPU正在执行的指令,

敲门这个信号就相当于中断引脚上的电位变化,

做完这道题再去开门就相当于CPU执行完当前一条指令然后再去处理中断,

询问访客姓名和敲门合起来,相当于CPU确定事件号,

招待客人的过程相当于执行中断处理程序

客人走了继续做下一道题相当于中断处理完成之后返回下一条指令

综上,中断处理过程的流程图表示为

image-20220518212631468

从数学作业的角度来说,中间招待客人的时候,数学作业没有被处理,但是作业本子也不知道这个做题的是玩电脑去了还是蹲坑去了还是招待客人去了.反正这不重要.重要的是不管中间干了啥,数学作业都是被完整做完的.

因为访客和做作业是异步的,所以招待客人和数学作业玩不玩成一点关系都没有

从正在执行的进程角度看,发生中断被抢占CPU和被其他进程抢占CPU没有区别,进程也没有因为中断被改变什么

陷阱(系统调用)

陷阱又可以理解为软件主动产生的"软中断",是有意为之的

系统调用,陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,又叫做系统调用。

在网上搜索的时候,感觉有一种广义的"系统调用",表示一整个从用户程序到硬件然后再返回用户程序的过程

感觉还有一种狭义的系统调用,就是导致CPU从用户态改变到内核态那关键的一条汇编指令int $0x80

系统级函数,比如open(),read(),close()等等,涉及IO操作,显然只在用户态是干不成事情的,需要使用内核的资源,需要陷入内核态,因此需要去执行系统调用

我们写的程序怎么陷入内核态呢?在c源代码层面上只需要调用glibc中的系统级函数

汇编语言层面观察系统级函数write的实现

CSAPP上给出的x86-64linux系统调用号表(感觉这里系统级函数和系统调用的概念有些混淆,应该是广义的系统调用)

image-20220518215022569

调用write

image-20220519074321305

系统调用号用rax传递

文件描述符用rdi传递

字符串用rsi传递

字符串长度用rdx传递

syscall即侠义的系统调用

用户视角下系统调用过程示意图

image-20220519073514392

程序员视角意味着"陷阱处理程序"是一个抽象的概念,程序员知道系统要做这么一个陷阱处理,但是具体做了啥程序员不知道,

程序员可以知道的是自己的程序会调用syscall,然后CPU的使用权就不属于自己的程序了,而是属于陷阱处理.程序员还知道的是陷阱处理之后的结果,比如调用write之后将缓冲区char buffer[20];中的字符打印到了屏幕上

废话:为什么要有陷阱(系统调用)这种异常?

image-20220522191723763

c库函数,系统级函数,系统调用的关系

image-20220522170002447

c库函数

libc和glibc都是Linux下的c函数库

glibc是linux下面c标准库的实现,即GNU C Library。glibc本身是GNU旗下的C标准库,后来逐渐成为了Linux的标准c库,而Linux下原来的标准c库Linux libc逐渐不再被维护。Linux下面的标准c库不仅有这一个,如uclibc、klibc,以及上面被提到的Linux libc,但是glibc无疑是用得最多的。glibc在/lib目录下的.so文件为libc.so.6。

c函数库中的函数非常多,可以按照有没有涉及系统调用进行分类

涉及系统调用的printf,scanf,malloc,free等,这些函数都是系统级函数,这些函数执行系统调用陷入内核

不涉及系统调用的itoa,strstr

关于库函数和内核函数的区别,这个问题中文站点搜了好多,全在扯淡,最后参考了 stackoverflow

比如内核函数sys_fork就是c库函数fork系统调用入口,即当我们的的代码中使用fork函数的时候,fork函数会去自己调用sys_fork而不是调用sys_write

为啥要这样套娃呢?

因为sys_call是内核函数,依赖于系统

但是fork是c库函数,要求POSIX可移植

这就好似协议分层,fork所在高层只管调用内核提供的一个叫sys_fork函数,内核具体怎么实现这个函数不关心

而内核也不知道上层会有什么,只管根据人的需要涉及sys_fork的参数返回值,功能等等

fork函数大体的调用链(踢皮球链)

1
fork() -> glibc wrapper -> raw syscall invocation -> transition to kernel mode -> syscall lookup -> sys_fork() -> do_fork().

内核函数

kernel函数即内核函数

Assembly - System Calls (tutorialspoint.com)给出了一个x86linux上调用write时,在内核函数层面发生的事情

x86linux内核函数表:

image-20220518215333246
1
2
3
4
5
mov	edx,4		; message length						;要打印的信息长度用edx传递
mov ecx,msg ; message to write ;要打印的信息msg用ecx寄存器传递
mov ebx,1 ; file descriptor (stdout) ;文件描述符,魔数1表示标准输出,即显示器
mov eax,4 ; system call number (sys_write) ;eax存放内核函数号,决定调用sys_write函数
int 0x80 ; call kernel ;陷入内核,根据先前放在eax中的系统调用号决定执行什么命令

系统命令和系统调用的关系

系统命令比如ls,ifconfig,mv,cp等等是由一个或者多个c库函数实现的,可能其中会用到系统调用.系统命令和系统调用之间还有一段距离

CSAPP上就有练习题让我们用<stdio.h>等头文件里面c库函数写一个mv命令之类的

操作系统视角下的系统调用过程

用户进程通过eax寄存器将内核函数号交给system_call函数,这时已经下到kernel模式了

system_call函数的作用是,根据内核函数号去查system_call_table表,执行相应的sys_开头的内核函数,

内核函数执行完毕之后执行syscall_exit返回到system_call函数

system_call函数执行resume_userspace返回用户空间

系统调用的整个过程

image-20220522212228880

例如getpid函数系统调用的简化过程

image-20220522210953699
1
2
3
4
getpid()
int 0x80
system_call
sys_getpid()

实验:添加系统调用

添加一个系统调用不只是向系统调用号表中添加一个表项,要考虑内核中整个系统调用过程

1.根据系统调用类型查系统调用号表获得系统调用号

2.系统调用号查内核函数跳转表找到应该执行的内核函数

3.执行内核函数

各部分的具体作用和位置如下

系统调用号表unistd_32.h

前置知识:c语言宏定义

在我校的操作系统实验中,使用的操作系统内核版本是linux-2.6.32.60

/usr/src/linux-2.6.32.60/arch/x86/include/asm/unistd_32.h位置

该文件中全是宏定义,形如#define __NR_exit 1,将每一个系统调用号魔数定义为一个有实际意义的字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H

/*
* This file contains the system call numbers.
*/

#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
...

#define NR_syscalls 337 //系统调用总个数

...

感觉类似于DNS协议

将ip地址号映射到一个方便人类记忆的域名

将一个域名解析到一个ip地址号

内核函数跳转表syscall_table_32.S

前置知识x86汇编语言

.S:汇编语言源程序;预处理,汇编

也就是说该文件是汇编写的

内核函数跳转表以系统调用好表中的系统调用号为下标,总个数也是在unistd_32.h中宏定义的#define NR_syscalls 337

系统调用号和内核函数跳转表项是一个萝卜一个坑的关系,修改系统调用号就得修改跳转表项,因此现有的不要乱改

如果系统调用号没有对应下标的内核跳转表表项,则默认指向函数调用表中,教材中说是sys_ni_syscall()

在我校的操作系统实验课程的安排中,这个内核函数跳转表的位置在/usr/src/linux-2.6.32.60/arch/x86/kernel/syscall_table_32.S

1
2
3
4
5
6
7
8
9
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid

这个跳转表也比较类似CSAPP第三章上介绍的switch跳转表

image-20220523075250116

系统调用号表和内核函数跳转表有一一对应关系

image-20220522215928689
内核函数声明syscalls.h

该声明在/usr/src/linux-2.6.32.60/include/linux/syscalls.h

就是一个头文件,里面都是函数声明,在链接时起到引用的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
asmlinkage long sys_time(time_t __user *tloc);
asmlinkage long sys_stime(time_t __user *tptr);
asmlinkage long sys_gettimeofday(struct timeval __user *tv,
struct timezone __user *tz);
asmlinkage long sys_settimeofday(struct timeval __user *tv,
struct timezone __user *tz);
asmlinkage long sys_adjtimex(struct timex __user *txc_p);

asmlinkage long sys_times(struct tms __user *tbuf);

asmlinkage long sys_gettid(void);
asmlinkage long sys_nanosleep(struct timespec __user *rqtp, struct timespec __user *rmtp);
...

关于asmlinkage修饰符:

需要前置知识,x86汇编语言和调用约定

类似位置的修饰符我们见过__cdecl,__fastcall,这里asmlinkage也是一种调用约定的修饰符,试想如果不声明该修饰符,则linux上按照System V AMD64 ABI约定的函数传参方法,前六个参数是通过edi,esi,edx,ecx,r8d,r9d这六个寄存器传递的,返回值是通过eax寄存器传递的.

而对于内核函数

asmlinkage是一个宏定义#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0))),

其作用是使用eax,ebx,ecx传递参数,eax始终传递系统调用号(内核函数号)

eax传递内核函数号

比如write函数从用户态下到内核态system_call时的调用过程

1
2
3
4
5
mov	edx,4		; message length						;要打印的信息长度用edx传递
mov ecx,msg ; message to write ;要打印的信息msg用ecx寄存器传递
mov ebx,1 ; file descriptor (stdout) ;文件描述符,魔数1表示标准输出,即显示器
mov eax,4 ; system call number (sys_write) ;eax存放内核函数号,决定调用sys_write函数
int 0x80 ; call kernel ;陷入内核,根据先前放在eax中的系统调用号决定执行什么命令
内核函数实现sys.c

内核函数的定义实现在/usr/src/linux-2.6.32.60/kernel/sys.c

该源文件开幕就是版权声明,是龙得卧着,是虎得盘着,我Linus是什么人不用我自己说

1
2
3
4
5
/*
* linux/kernel/sys.c
*
* Copyright (C) 1991, 1992 Linus Torvalds
*/

然后include了一大堆头文件,其中就有syscalls.h

1
2
3
4
5
#include <linux/module.h>
.....
#include <linux/syscalls.h>
....
#include <asm/unistd.h>

然后就见鬼了,一个sys_开头的函数实现都没有找到,这样syscalls.h岂不是include了个寂寞

syscalls.h的宏定义中我们可以找到答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define SYSCALL_DEFINE0(name)	   asmlinkage long sys_##name(void)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
...
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

...

#define SYSCALL_DEFINE(name) asmlinkage long sys_##name
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)); \
static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__)); \
asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__)) \
{ \
__SC_TEST##x(__VA_ARGS__); \
return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__)); \
} \
SYSCALL_ALIAS(sys##name, SyS##name); \
static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

asmlinkage long sys_##name这种格式的函数接口都被统一地宏定义为__SYSCALL_DEFINEx(x, name, ...)

统一定义成SYSCALL_DEFINE0SYSCALL_DEFINE6这7种宏定义,作用是是实现起来方便

sys.c中就有__SYSCALL_DEFINEx(x,name,...)这类函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SYSCALL_DEFINE3(setpriority, int, which, int, who, int, niceval)
{
...
}

/*
* Ugh. To avoid negative return values, "getpriority()" will
* not return the normal nice-value, but a negated value that
* has been offset by 20 (ie it returns 40..1 instead of -20..19)
* to stay compatible.
*/
SYSCALL_DEFINE2(getpriority, int, which, int, who)
{
...
}

我们自定义的内核函数又少又简单,没有必要也遵守这种宏定义,直接在sys.c中按照syscalls.h中的函数声明去实现函数即可.反正宏定义只是起别名,叫绰号和叫原名都不会错

我们需要做的

考虑上述各部分的作用,我们需要做的

1.添加系统调用表unistd_32.h

修改之前:

image-20220522222645579

格式比着葫芦画个瓢,修改之后

QQ截图20220522222731

注意下面#define NR_syscalls 338也得跟着改,

这个值表示的是系统调用的总数,由于系统调用号从0开始编号,因此当我们新增一个337号时,总数有338个

2.添加内核函数跳转表syscall_table_32.S表项

QQ截图20220522223100

只需要在最后一行添加一项,格式比着葫芦画个瓢

3.在syscalls.h中增加一条函数声明

注意以root打开文件才有权限修改

image-20220523073622059

4.在sys.c中增加内核函数的实现

image-20220523074428529

这里最后手残写了一个S一开始没发现,编译就是不通过,在形成sys.o时报错,回来看笔记才发现多一个S

5.编译内核

前置知识

linux内核

链接

makefile的编写

我几乎都不会

学校给的实验环境中写好了makefile,好长一个,整个内核的编译,我唧己啃腚写不出来.

并且makefile涉及链接操作,在CSAPP第七章有比较详细的介绍,这是后话了

然后就是makefile的语法,这我目前也不会,这也是后话了

总之就是

1
2
3
4
make menuconfig #修改一下内核名称这种无关紧要的东西,其他的不敢改也不会改,在这里我
make #执行makefile文件,开始漫长的编译过程
make modules_install
make install

如果上述四步都能完整执行,真的烧高香了

编译完了会形成一个vmlinux.o目标模块,链接后会形成vmlinux这么一个32位ELF可执行文件

image-20220523115115233

后面两步完了之后会在/lib/modules下面生成

QQ截图20220523115917

2.6.32-28-generic是系统原来自带的

2.6.32.60XXXXXXXXXXXXh是实验一生成的

2.6.32.60XXXXXXXXXXXXf是本次实验生成的

然后使用update-initramfs -c -k 2.6.32.60XXXXXXXXXXf生成"虚拟盘文件"

我们正在玩一个大型橙光游戏,动辄编译个把小时,以防万一,此时拍一个快照吧

image-20220523121014189

6.修改grub.cfg

grub.cfg/boot/grub/grub.cfg

比着葫芦画瓢,照抄一个稍微改一下

QQ截图20220523120814

完了保存重启

7.验证

/mnt/hgfs/share/test.c中这样写

选这么一个位置纯粹是因为共享文件夹,可以在本机直接用vscode编写,虚拟机输入个字符都卡的要死

1
2
3
4
5
#include <stdio.h>
int main(){
printf("test sjf's syscall,%d\n",syscall(337,13));//调用337号系统调用
return 0;
}
image-20220523130855962

然后使用dmesg命令查看我们在自定义的内核函数中的输出printk,最后一行是这样的

image-20220523131010973

这与我们编写的是相同的,证明我们自己新增的系统调用的整个过程奏效了

故障

由错误情况引起,可能被故障处理程序修正.

image-20220519073802747

比如缺页异常

终止

发生致命错误,程序直接寄掉,处理程序将控制返回abort历程

image-20220519073854260

异常号

发现异常的时候会用事件号查事件表.这里的异常号不是事件号,而是对每个异常都进行编号

0~31号异常由Intel架构师定义

32~255号异常由操作系统设计师定义

image-20220519074127003

进程

进程上下文:程序正确运行所需的状态组合,包括堆栈,代码和数据,通用寄存器,程序计数器,环境变量,打开的文件描述符集合

私有虚拟地址空间

用户栈往下的部分都是进程独立的虚拟地址空间

image-20220519075052410

私有虚拟地址空间,不是私有物理地址空间.

共享库在主存中只有一块物理地址空间,但是在两个进程虚拟地址空间中映射到不同部分

用户态和内核态

设置两种状态的作用是,限制进程对内核数据结构的访问修改,只有操作系统进程可以运行在内核态.用户应用进程永远不可能运行在内核态,用户应用进程只能通过系统调用,请操作系统去完成目的.

异常处理程序都运行在内核态

用户态和内核态怎么区分的?通过CPU中某个控制寄存器中的某个模式位

上下文切换

高层次的异常

进程上下文包括堆栈,数据和代码,各种寄存器,程序状态字,内核栈,内核各种数据结构比如页表

上下文切换的意思是,挂起当前正在运行的进程,保存其运行现场,然后执行其他进程.当该进程再次被调度时还原其运行现场

发生上下文切换时,操作系统会1.保存当前进程的上下文(放在内存里),2.恢复先前某个被挂起的进程执行现场3.控制交给该进程

上下文切换发生的时机:

1.系统调用可能引发上下文切换,比如当前进程使用系统级函数write向标准输出打印,这个过程对于CPU来说非常漫长.CPU会切换到执行另一个进程,不会等待数据写到显示器.更加明显的是sleep系统调用,不妨把话说得更明白些,直接让进程睡觉.

image-20220519080431478

2.软件中断,进程时间片用光了,该让给另一个进程执行了.

在程序员视角,进程的状态只有三种

image-20220519082556372

进程控制

控制进程的函数都是系统级函数

获取进程ID

1
2
3
#define pid_t int
pid_t getpid(void); //获取当前进程id
pid_t getppid(void); //获得父进程id

proc.c

1
2
3
4
5
6
7
8
9
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
int pid=getpid();
int ppid=getppid();
printf("pid=%d,ppid=%d",pid,ppid);
return 0;
}

在bash shell命令行上编译运行

1
2
3
4
5
6
7
8
9
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ps
PID TTY TIME CMD
9 pts/0 00:00:00 bash
41 pts/0 00:00:00 ps

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
pid=42,ppid=9

bash调用ps程序,因此bash是ps的父进程

由于proc进程是由bash创建的,因此bash是proc的父进程

进程号只会越来越大,不会重复利用一个已经完成的进程的进程号

创建进程

1
pid_t fork(void);//子进程返回0,父进程返回子进程pid

非常疑惑的一点是为什么一个函数调用可以返回两次

对两个进程分别返回一次

proc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("in father process 0\n"); //fork之前只会被父进程执行一次
int pid=fork(); //此处子进程和父进程并行
if(pid==0){ //对于子进程来说,它确实有一个正整数进程号,但是fork返回的不是
printf("in son process 1\n");
}
if(pid!=0){
printf("in father process 1\n");
}
}

运行结果:

1
2
3
4
5
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process 0
in father process 1
in son process 1

不管什么进程,其进程号都是正数,不可能是0.fork对子进程的返回值为0并不代表一个进程号,而是区分子进程和父进程的标志

根据fork的返回值不同,这是一模一样的代码区分是父进程在执行还是子进程在执行的唯一标志

这里c风格fork创建进程和C++中使用thread创建线程差别很大

thread创建线程,只需创建一个thread对象,对其构造函数传递一个函数,后面该函数就会自己开一条线程执行,thread对象就是线程的句柄.可以在函数线程之外,比如主线程处,通过thread对象,很自然地操作线程的行为比如detach或者join

而在这里唯一能区分线程的句柄就是一个整数pid,并且这个pid位于进程之中,只能在运行时通过pid判断是哪一个进程.不能在进程之外操作进程的行为

fork之后原来的进程照旧执行,一个新的进程会拥有原进程的一模一样的虚拟地址空间的拷贝,包括代码数据寄存器堆栈等等.子进程和父进程的虚拟地址空间相互独立

proc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int global=10;
int main(){
int local=20;
int pid=fork();
if(pid==0){
printf("in son process: ");
}
else{
printf("in father process: ");
}
printf("global=%d,local=%d\n",global++,local++);//这里有修改

}
1
2
3
4
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process: global=10,local=20
in son process: global=10,local=20 #两个打印相同说明global有两个,local有两个

父进程和子进程都打印到屏幕说明父子进程共享父进程已经打开的文件描述符1

用进程图描述fork是比较直观的

image-20220519084511671
进程图如何实现

1.main上多个分支

image-20220519092351240
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){

int pids[5];
int fpid=getpid();//fpid在fork之前先计算好,此后即使所有子进程都拷贝,也只是拷贝的父进程号
if(fpid==getpid()){//getpid在每个进程都不同,只有父进程中才会有fpid=getpid
for(int i=0;i<5;++i){
pids[i]=fork();//实际上后来的子进程的pids也会存有数据,原因是父进程在创建第i个子进程时,pids已经写入前i-1个子进程号了
}
}
if(fpid==getpid()){//getpid在每个进程都不同,只有父进程中才会有fpid=getpid
printf("in father process,pid=%d\n",fpid);
for(int i=0;i<5;++i){
printf("pid%d=%d,",i,pids[i]);
}
printf("\n");
}
return 0;

}

这样实际上的进程图

image-20220519093748088

运行结果:

1
2
3
4
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process,pid=222
pid0=223,pid1=224,pid2=226,pid3=230,pid4=235,

2.main和第一个子进程同时分支

image-20220519093425314

这个很容易实现

1
2
fork();
fork();
fork前后

proc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int global=10;
int main(){
int local=20;
int pid0=getpid(); //fork之前getpid
int forkid=fork(); //forkid只是用来
int pid1=getpid(); //fork之后getpid

if(forkid==0){
printf("in son process,pid0=%d,pid1=%d,forkid=%d\n",pid0,pid1,forkid);
}
else{
printf("in father process,pid0=%d,pid1=%d,forkid=%d\n",pid0,pid1,forkid);
}
return 0;

}

运行结果:

1
2
3
4
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process,pid0=116,pid1=116,forkid=117
in son process,pid0=116,pid1=117,forkid=0

不管是父进程还是子进程,pid0=116相同,而pid1却不同,这是因为,pid0是fork之前执行的,当fork执行时,pid0已经被计算出了,作为一个局部变量压栈了,子进程不会再去计算pid0,而是从父进程堆栈的拷贝上直接拿.

但是pid1的情况不同,fork之后,子进程已经获得了父进程堆栈的拷贝,此后两个进程地址空间独立,后来的pid1就是分别计算之后分别存放在自己的堆栈里

看起来像是数据不管是fork前后都会被复制,实际上只复制了fork前的数据,此后进程各自维护自己的数据

终止进程

1
void exit(int status);//以status状态码返回

proc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
int forkid=fork();
int pid=getpid();
if(forkid==0){
printf("in son process,pid=%d\n",pid);
exit(0); //让子进程结束运行
}
else{
printf("in father process,pid=%d\n",pid);
}

printf("process %d is still running\n",pid); //此句打印表明还在运行的进程

return 0;
}
1
2
3
4
5
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process,pid=134
process 134 is still running
in son process,pid=135

回收子进程

进程终止之后并不会立刻消失地无影无踪,而是处于一种等待被父进程回收的状态,父进程回收终止子进程时,内核将子进程的exit状态传递给父进程.子进程被回收后才会消失地无影无踪

如果父进程一直没有回收已经终止的子进程,子进程就一直存在,称为"僵死进程"

如果父进程提前结束呢?内核会安排init进程成为孤儿进程的父进程

这就好比未成年的孩子父母双亡,被警察局送给孤儿院收养

这里起孤儿院作用的init进程,其pid=1,在系统启动时被内核创建,除非关机,否则永不终止.是所有进程的老祖宗.孤儿进程终止后,init会回收之

waitpid
1
pid_t waitpid(pid_t pid,int *statusp,int options);

Attention!!!当第三个参数options不设置的时候,函数的默认行为是:挂起调用进程,直到有满足条件的子进程终止

参数意义:

1.pid_t pid

如果pid>0则等待该指定pid的子进程终止

如果pid=-1则等待该进程的所有子进程,如果有其中的一个终止则waitpid返回该终止子进程的pid

proc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
int fpid=getpid();
int forkid=fork();
if(forkid==0){//子进程中
printf("in son process,id=%d\n",getpid());
int n=1000000;
while(n--);//拖延时间
exit(0);
}
else{//父进程中
printf("in father process,id=%d\n",fpid);
waitpid(forkid,0,0);//指定等待唯一的子进程返回 //只指定第一个参数,其他使用缺省值
printf("son process %d exit\n",forkid);
}
printf("process %d is still running\n",getpid());
return 0;
}

运行结果:

1
2
3
4
5
6
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process,id=273
in son process,id=274 #父进程需要等待子进程完成
son process 274 exit
process 273 is still running

2.int *stausp

如果statusp非空,则waitpid就会在statusp中记录子进程的exit status,使用指针引用传递

proc.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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
int fpid=getpid();
int forkid=fork();//区分父子进程
if(forkid==0){//子进程中
printf("in son process,id=%d\n",getpid());
int n=1000000;
while(n--);//拖延时间
exit(1);//子进程以status=0状态终止
}
else{//父进程中
int status=999;//设置status初始值
printf("in father process,id=%d\n",fpid);
waitpid(forkid,&status,0);//使用status承载子进程的exit状态值 //缺省第三个参数
printf("son process %d exit with status= %d\n",forkid,status);
printf("WIFEXITED(status)=%d\n",WIFEXITED(status));
printf("WEXITSTATUS(status)=%d\n",WEXITSTATUS(status));
printf("WIFSIGNALED(status)=%d\n",WIFSIGNALED(status));
printf("WTERMSIG(status)=%d\n",WTERMSIG(status));
printf("WIFSTOPPED(status)=%d\n",WIFSTOPPED(status));
printf("WSTOPSIG(status)=%d\n",WSTOPSIG(status));
printf("WIFCONTINUED(status)=%d\n",WIFCONTINUED(status));
}

printf("process %d is still running\n",getpid());

return 0;

}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./proc
in father process,id=41
in son process,id=42
son process 42 exit with status= 256
WIFEXITED(status)=1
WEXITSTATUS(status)=1 #这是exit(status)中的status
WIFSIGNALED(status)=0
WTERMSIG(status)=0
WIFSTOPPED(status)=0
WSTOPSIG(status)=1
WIFCONTINUED(status)=0
process 41 is still running

解释status的几个宏定义

image-20220521075243518

奇怪的是,status明明是一个整数,为什么还能使用类似函数调用的宏,得到不同的结果?

推测status这个双字整形的每一位都携带着某种信息,实际上相当于一个布尔值的返回值集合,这些宏定义通过按位运算相当于在这个返回值集合中取了一部分值做运算

3.int options修改子进程的处理方式

waitflags.h中有这么几个宏定义

1
2
3
/* Bits in the third argument to `waitpid'.  */				//waitpid的第三个参数 其中的一些位
#define WNOHANG 1 /* Don't block waiting. */ //01
#define WUNTRACED 2 /* Report status of stopped children. */ //10

函数的默认行为是:挂起调用进程,直到有满足条件的子进程终止

指定options之后,函数的行为:

options 作用
WNOHANG 如果指定的子进程或者等待集合中的子进程都没有终止则立即返回0
WUNTRACED 挂起父进程,直到等待集合中的一个进程变成已终止或者被停止,返回该子进程pid
WCONTINUED 挂起父进程,直到等待集合中一个正在运行的进程终止或者等待集合中一个被停职的进程收到SIGCONT信号重新开始
WNOHANG | WUNTRACED 立即返回.如果等待集合中的子进程都没有被停止或者终止,返回0.如果有一个子进程停止或者终止,返回该子进程pid

如果调用waitpid的进程没有任何子进程则waitpid返回-1,并设置errno=EINTR

wait
1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *statusup);

wait(&status)等价于waitpid(-1,&status,0)

父进程挂起,等待子进程之一终止则返回其pid

sleep
1
2
#include <unistd.h>
unsigned int sleep(unsigned int secs);

休眠secs秒,睡够了觉则sleep返回0否则返回还要睡多久

pause
1
2
#include <unistd.h>
int pause(void);

让调用者进程休眠,直到该进程接收到信号

加载并运行程序execve

1
2
#include <unistd.h>
int execve(const char * filename,const char *argv[],const char * envp[]);//成功则不返回,失败则返回-1

执行filename指向的文件,参数为argv,环境为envp

例如:

execve.c

1
2
3
4
5
6
#include <stdio.h>
#include <unistd.h>
int main(){
execve("/bin/sh",0,0);
return 0;
}
1
2
3
4
kali@Executor:/mnt/c/Users/86135/desktop/os$ gcc execve.c -O0 -o execve

kali@Executor:/mnt/c/Users/86135/desktop/os$ ./execve
$

执行之后shell由bash换成了sh

关于参数const char *envp[]

image-20220521094616486

必须使用get或者set方法访问或者修改环境

1
2
3
4
5
#include <stdlib.h>
char *getenv(const char *name);//返回name键对应的value值
int putenv(char *str);//这里str的格式为name=value,将[name,value]键值对放在环境表中,如果name键存在则覆盖之
int setenv(const char *name,const char *newvalue,int overwrite);//[name,newvalue]键值对放在环境表中,如果name键存在则根据overwrite是否为1决定是否覆盖
int unsetenv(const char *name);//清除环境表中的[name,value]键值对,如果name键不存在则什么都不会发生

myecho.c

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

int main(int argc,char *argv[]){

printf("%s\n",getenv("HOME"));//home起始目录
printf("%s\n",getenv("SHELL"));//用户首选shell名
printf("%s\n",getenv("PWD"));//当前工作目录绝对路径

setenv("SHELL","/bin/sh",0);//将用户首选的shell改成/bin/sh,overwrite=0表示如果已经存在name=SHELL的键则啥也不干
printf("%s\n",getenv("SHELL"));

setenv("SHELL","/bin/sh",1);//将用户首选的shell改成/bin/sh,overwrite=1表示如果已经存在name=SHELL的键则覆盖原来的value
printf("%s\n",getenv("SHELL"));
return 0;
}
1
2
3
4
5
6
7
8
9
10
>┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ gcc myecho.c -Og -o myecho

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./myecho
/home/kali
/bin/bash
/mnt/c/Users/86135/desktop/os
/bin/bash
/bin/sh

setenv还可以新建环境变量,如果name键没有找到则新建环境变量

环境变量表

image-20220521100230271

在当前进程中修改环境变量对当前进程无效,但是对该进程后续建立的子进程有效

这里注意putenvsetenvoverwrite=1时的区别

一是参数的格式,putenvchar *str让写的是name=value这种格式,而setenvnamevalue分开成为两个参数

二是参数的类型,putenv中的参数没有const修饰,这就意味着str是可以被修改的

setenv中的name和newvalue都带有const修饰,不可修改

实际上putenv不会为新的环境变量另外开空间,而是直接把传入的参数(不管是堆上还是栈上还是全局的)填入环境表

setenv则会另开空间拷贝一份环境变量放进环境表

比如下面例子

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

int main(int argc,char *argv[]){
char str[]="myid=deutschball";
putenv(str);
printf("%s\n",getenv("myid"));
strcpy(str,"myid=dustball"); //此处修改str将会导致环境表中键myid的值变化
printf("%s\n",getenv("myid"));

char name[]="myunit";
char value[]="empire";
setenv(name,value,0);
printf("%s\n",getenv(name));
strcpy(value,"rebel");
printf("%s\n",getenv(name));


return 0;
}
1
2
3
4
5
6
7
8
9
┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ gcc myecho.c -Og -o myecho

┌──(kali㉿Executor)-[/mnt/c/Users/86135/desktop/os]
└─$ ./myecho
deutschball
dustball #此处值发生了变化
empire
empire #此处值不发生改变

此处应有一个编个shell的实验,但是工程量太大,现在不想写,留作后话吧

数据结构在汇编语言下的表现

数组

栈上数组

main.c

1
2
3
4
5
6
7
8
9
10
11
12
void func(){
int local_array[3];
int index=2;
local_array[0]=10;
local_array[1]=20;
local_array[2]=30;
local_array[index]=40;
}
int main(){
func();
return 0;
}
1
2
gcc main.c -O0 -o main.exe
ida64 main.exe #需要将ida的根目录添加到环境变量path

func函数的反汇编:

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
.text:0000000000401560
.text:0000000000401560 ; =============== S U B R O U T I N E =======================================
.text:0000000000401560
.text:0000000000401560 ; 64位windows上不再有__fastcall和_cdecl等调用约定的区别,只有一个同一的微软x64调用约定
.text:0000000000401560 ; Attributes: bp-based frame
.text:0000000000401560
.text:0000000000401560 ; void __fastcall func()
.text:0000000000401560 public func
.text:0000000000401560 func proc near ; CODE XREF: main+D↓p
.text:0000000000401560
.text:0000000000401560 var_10 = dword ptr -10h
.text:0000000000401560 var_C = dword ptr -0Ch
.text:0000000000401560 var_8 = dword ptr -8
.text:0000000000401560 var_4 = dword ptr -4
.text:0000000000401560
.text:0000000000401560 push rbp
.text:0000000000401561 mov rbp, rsp
.text:0000000000401564 sub rsp, 10h ; 栈上开0x10=16字节的空间,用于存放局部变量(int local_array[3] 数组和int index变量,恰好4个int*一个int占用4字节)
.text:0000000000401568 mov [rbp+var_4], 2 ; int index=2,可以判断var_4变量对应index
.text:000000000040156F mov [rbp+var_10], 0Ah ; 0x0Ah=10,对应local_array[0]=10;可以判断var_10对应local_array[0]
.text:0000000000401576 mov [rbp+var_C], 14h ; 同理var_C=local_array[1]
.text:000000000040157D mov [rbp+var_8], 1Eh ; 同理var_8=local_array[2]
.text:0000000000401584 mov eax, [rbp+var_4] ; 把index变量放在寄存器中,为0x401589的寻址做准备
.text:0000000000401587 cdqe ;将eax寄存器拓展为rax寄存器,该指令只作用于eax寄存器
.text:0000000000401589 mov [rbp+rax*4+var_10], 28h ; local_array[index]=40
.text:0000000000401591 nop
.text:0000000000401592 add rsp, 10h ;函数执行完毕,退栈
.text:0000000000401596 pop rbp ;还原rbp原始功能
.text:0000000000401597 retn
.text:0000000000401597 func endp
.text:0000000000401597

函数栈帧基于rbp帧指针,主函数没有对其传递参数,栈帧中 有返回值,rbp保存值,还有四个变量

image-20220502203420528
image-20220502204300823

源程序中写的数组int local_array[3]被拆成了3个独立的int变量

只有在.text:0000000000401589 mov [rbp+rax*4+var_10], 28h这里可以隐约看出实在对数组进行寻址操作

var_10(rbp,rax,4)=M[rbp+rax*4+var_10]

rax中存放的是index的值,用rax*4是因为一个int占4字节,rbp+var_10是数组的基地址,rax*4是偏移量

在分析出var_8,var_C,var_10同属于一个基地址var_10的数组结构后,可以在func函数的栈帧视图下高亮这三个变量,使用右键菜单的array选项将其合并为一个数组

image-20220502205958995
image-20220502205827491

​ 合并后回到反汇编视图

image-20220502210142961

发现三个变量合并为一个var_10,其地址rbp-10h,刚好和var_4相差0xC=12字节即三个int类型变量

然后可以在反汇编视图下使用右键菜单的rename功能将变量命名有意义

image-20220502210513069

rename之后后文相关位置都会重新命名

全局数组

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
int global_array[3];
void func(){

int index=2;
global_array[0]=10;
global_array[1]=20;
global_array[2]=30;
local_array[index]=40;
}
int main(){
func();
return 0;
}

func函数的反汇编:

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
.text:0000000000401560
.text:0000000000401560 ; =============== S U B R O U T I N E =======================================
.text:0000000000401560
.text:0000000000401560 ; Attributes: bp-based frame ;局部变量和参数在栈帧中的地址基于rbp帧指针
.text:0000000000401560
.text:0000000000401560 ; void __fastcall func()
.text:0000000000401560 public func
.text:0000000000401560 func proc near ; CODE XREF: main+D↓p
.text:0000000000401560
.text:0000000000401560 var_4 = dword ptr -4 ;局部变量只有一个,只能对应index
.text:0000000000401560
.text:0000000000401560 push rbp
.text:0000000000401561 mov rbp, rsp
.text:0000000000401564 sub rsp, 10h
.text:0000000000401568 mov [rbp+var_4], 2 ;int index=2
.text:000000000040156F lea rax, global_array ;R[rax]=&global_array,将global_array的地址放在rax
.text:0000000000401576 mov dword ptr [rax], 0Ah ;寄存器寻址,然后dword ptr指定双字访问内存,global_array[0]=10
.text:000000000040157C lea rax, global_array ;重复R[rax]=&global_array,目的是防止上一次装载和本次之间rax有变化
.text:0000000000401583 mov dword ptr [rax+4], 14h ;寄存器+立即数寻址,双字访问内存
.text:000000000040158A lea rax, global_array
.text:0000000000401591 mov dword ptr [rax+8], 1Eh
.text:0000000000401598 lea rax, global_array
.text:000000000040159F mov edx, [rbp+var_4] ;将var_4的拷贝到edx寄存器中
.text:00000000004015A2 movsxd rdx, edx ;edx有符号拓展到rdx
.text:00000000004015A5 mov dword ptr [rax+rdx*4], 28h ; 基址比例变址寻址,然后放入40
.text:00000000004015AC nop
.text:00000000004015AD add rsp, 10h
.text:00000000004015B1 pop rbp
.text:00000000004015B2 retn
.text:00000000004015B2 func endp
.text:00000000004015B2

问题是global_array貌似没有体现出声明来就直接使用了,左键双击global_array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.bss:0000000000407970                 public global_array
.bss:0000000000407970 global_array db ? ; ; DATA XREF: func+F↑o
.bss:0000000000407970 ; func+1C↑o ...
.bss:0000000000407971 db ? ;
.bss:0000000000407972 db ? ;
.bss:0000000000407973 db ? ;
.bss:0000000000407974 db ? ;
.bss:0000000000407975 db ? ;
.bss:0000000000407976 db ? ;
.bss:0000000000407977 db ? ;
.bss:0000000000407978 db ? ;
.bss:0000000000407979 db ? ;
.bss:000000000040797A db ? ;
.bss:000000000040797B db ? ;
.bss:000000000040797C db ? ;
.bss:000000000040797D db ? ;
.bss:000000000040797E db ? ;
.bss:000000000040797F db ? ;

发现global_array是位于bss段的,确实global_array在声明的时候并没有初始化,就应该放在bss段

global_arraydb=define byte即字节为单位,在bss段留了0407970~040797F一共16个字节的空间

如果在func函数的反汇编视图下修改global_array的名字改成空即显示ida给他命的哑名unk_407970,unk为unknown未知的缩写

如果在源代码中不写全局函数global_array,改用三个int类型变量

1
2
3
4
5
6
7
8
9
10
11
12
int l0;
int l1;
int l2;
void func(){
l0=10;
l1=20;
l2=30;
}
int main(){
func();
return 0;
}

此时三个全局变量在bss段中的分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.bss:0000000000407970                 public l2
.bss:0000000000407970 l2 db ? ; ; DATA XREF: func+1E↑o
.bss:0000000000407971 db ? ;
.bss:0000000000407972 db ? ;
.bss:0000000000407973 db ? ;
.bss:0000000000407974 public l0
.bss:0000000000407974 l0 db ? ; ; DATA XREF: func+4↑o
.bss:0000000000407975 db ? ;
.bss:0000000000407976 db ? ;
.bss:0000000000407977 db ? ;
.bss:0000000000407978 public l1
.bss:0000000000407978 l1 db ? ; ; DATA XREF: func+11↑o
.bss:0000000000407979 db ? ;
.bss:000000000040797A db ? ;
.bss:000000000040797B db ? ;
.bss:000000000040797C db ? ;
.bss:000000000040797D db ? ;
.bss:000000000040797E db ? ;
.bss:000000000040797F db ? ;
.bss:0000000000407980 public __native_startup_state
...

很诡异的是三个变量的分布是没有顺序的,l2和l0都分到了4个db即4字节的空间,但是l1却分到了8字节的空间

如果试图使用*(&l0+2)来得到l2实际上会算得*(0x407974+2*sizeof(int))=*(0x40797C)

而实际上l20x407970,刚才指针运算得到的值是属于l1"管理"的

堆上数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
void func(){
int *heap_array=(int *)malloc(3*sizeof(int));
int index=2;
heap_array[0]=10;
heap_array[1]=20;
heap_array[2]=30;
heap_array[index]=40;
free(heap_array);
}
int main(){
func();
return 0;
}

func函数反汇编

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
.text:0000000000401560
.text:0000000000401560 ; =============== S U B R O U T I N E =======================================
.text:0000000000401560
.text:0000000000401560 ; Attributes: bp-based frame
.text:0000000000401560
.text:0000000000401560 public func
.text:0000000000401560 func proc near ; CODE XREF: main+D↓p
.text:0000000000401560
.text:0000000000401560 var_C = dword ptr -0Ch ;var_C显然为index
.text:0000000000401560 Block = qword ptr -8 ;Block为堆上申请空间的指针
.text:0000000000401560
.text:0000000000401560 push rbp
.text:0000000000401561 mov rbp, rsp
.text:0000000000401564 sub rsp, 30h ;蜜汁操作,栈上分配了0x30h=48字节的巨大空间,但是只有两个局部变量
.text:0000000000401568 mov ecx, 0Ch ; Size,第一个参数使用ecx寄存器传递,这里0x0C=12字节相当于给出了堆上数组的大小
.text:000000000040156D call malloc ;malloc遵守 微软64位调用约定
.text:0000000000401572 mov [rbp+Block], rax ;rax寄存器带着malloc函数的返回值,即堆上地址的指针,赋值给Block
.text:0000000000401576 mov [rbp+var_C], 2 ;int index=2
.text:000000000040157D mov rax, [rbp+Block] ;将堆上指针放到rax中
.text:0000000000401581 mov dword ptr [rax], 0Ah ;字访问rax指向的地址,放入10
.text:0000000000401587 mov rax, [rbp+Block] ;将堆上指针再次放到rax中
.text:000000000040158B add rax, 4 ;rax中的指针副本后移4字节,恰好移过一个int
.text:000000000040158F mov dword ptr [rax], 14h ;单字访问rax指向的地址,放入20
.text:0000000000401595 mov rax, [rbp+Block]
.text:0000000000401599 add rax, 8
.text:000000000040159D mov dword ptr [rax], 1Eh
.text:00000000004015A3 mov eax, [rbp+var_C] ;将index放到eax中
.text:00000000004015A6 cdqe ;拓展eax到rax
.text:00000000004015A8 lea rdx, ds:0[rax*4] ;将ds:0+rax*4这个地址放到rdx寄存器
.text:00000000004015B0 mov rax, [rbp+Block] ;rax寄存器获得Block堆指针的拷贝
.text:00000000004015B4 add rax, rdx ;rax指向Block+4*rax即heap_array[index]
.text:00000000004015B7 mov dword ptr [rax], 28h ; 单字访问rax指向的地址,放入40
.text:00000000004015BD mov rax, [rbp+Block] ;rax获得Block堆指针拷贝
.text:00000000004015C1 mov rcx, rax ; rcx获得拷贝,准备作为参数传递给free函数
.text:00000000004015C4 call free
.text:00000000004015C9 nop
.text:00000000004015CA add rsp, 30h
.text:00000000004015CE pop rbp
.text:00000000004015CF retn
.text:00000000004015CF func endp
.text:00000000004015CF
The parts of program memory showing a stack variable pointing to dynamically allocated heap memory.

堆上数组的特征还是比较明显的

综上,使用变量下标访问数组时更容易察觉数组结构的存在,而使用常量下标访问数组时汇更像访问独立的变量

结构体

对齐

对齐在大多数情况下不是硬性要求,不对齐的话x86-64的硬件也是可以干活的,

但是为了提高性能intel建议对齐,编译器默认情况下是会自动对齐的

基本数据类型的对齐规则:

某种类型的对象,其地址必须是K的倍数

image-20220503010303191
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void func() {
char achar;
char bchar;
int aint;
printf("%x,%x,%x", &achar, &bchar, &aint);
return ;
}

int main() {
func();
return 0;
}
1
2
gcc -O0 -g -o main.out
gdb -tui -q main.out

使用gdb进行调试,观察运行时的堆栈

1.在func函数处下断点

image-20220503013023486

2.在断点处停下,打印观察

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) r
Starting program: /mnt/c/Users/86135/Desktop/reverse/mytest/main.exe

Breakpoint 1, func () at main.c:7
(gdb) p $rbp
$1 = (void *) 0x7fffffffdc90
(gdb) p &achar
$2 = 0x7fffffffdc8f ""
(gdb) p &bchar
$3 = 0x7fffffffdc8e ""
(gdb) p &aint
$4 = (int *) 0x7fffffffdc88

帧指针rbp指向0x7fffffffdc90

achar是一个char类型,无对齐要求,必然在紧接着帧指针下方0x7fffffffdc8f,bchar同理在achar下方0x7fffffffdc8e

但是aint并没有紧挨着bchar在0x7fffffffdc8d,而是在0x7fffffffdc88

栈向下增长,但是小端模式下,栈中的元素的起始位置是低地址,然后向高地址增长,比如aint就从0x7fffffffdc88然后向高地址增长直到0x7fffffffdc8b

func函数的栈帧:

地址 相对rbp的偏移量 存储内容
1 返回值地址
0x7fffffffdc90 0 rbp帧指针保存地址
0x7fffffffdc8f -1 achar
0x7fffffffdc8e -2 bchar
-3
-4
0x7fffffffdc8b -5 aint高位
-6 aint
-7 aint
0x7fffffffdc88 -8 aint低位
-9
-10
...
...

这里aint就没有从-6到-3,而是为了对齐采用-8到-5

小端模式

小端模式:假如int aint=10d=0xAh=0x0000 0000 0000 0000 0000 0000 0000 1010 b

那么0x7fffffffdc88这个低地址上应该存放aint的低八位即0000 1010b=0x10h

为了观察这个事情

使用几乎相同的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void func() {
char achar;
char bchar;
int aint=10;//注意此处给aint赋值10,小端模式下应该存放在&aint的第一个字节单元
printf("%x,%x,%x", &achar, &bchar, &aint);
return ;
}

int main() {
func();
return 0;
}

使用gdb在第7行下断点,然后运行到第七行的时候停下,打印&aint,结果为0x7fffffffe2e8,然后x/1 0x7fffffffe2e8打印从该地址开始的第一个单元(单位字节)

image-20220503020706566

结果为10,即证明低地址存放低位,小端模式

结构体的对齐规则

结构体各项目依然满足基本数据类型的对齐方式

结构体自身也有对齐要求,其==基地址必须是最大成员大小的整数倍==

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
typedef struct{
char name;
int from;
int to;
}Edge;
void func(){
char a ;
Edge e;
char b;
printf("%x,%x,%x",&a,&e,&b);
return ;
}
int main(){
func();
return 0;
}
1
2
gcc -O0 -g -o main.exe
gdb -tui -q main.exe

func函数处下断点然后在该断点处停下,打印观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p &e
$4 = (Edge *) 0x7fffffffdc80
(gdb) p &e.name
$5 = 0x7fffffffdc80 "" ;e.name的位置
(gdb) p &e.from
$6 = (int *) 0x7fffffffdc84 ;e.from的位置
(gdb) p &e.to
$7 = (int *) 0x7fffffffdc88 ;e.to的位置
(gdb) p &b
$8 = 0x7fffffffdc7f "" ;char b的位置
(gdb) p &a
$9 = 0x7fffffffdc8f "" ;char a的位置
(gdb) p $rbp
$10 = (void *) 0x7fffffffdc90 ;帧指针位置

func函数的栈帧

image-20220503111359404

首先在结构体内部安排对齐:

name放在偏移量为0的位置,然后int from放在偏移量为4的倍数的最小位置,显然是4,然后int to同理放在8,from和name之间就空出了3字节,原因就是考虑对齐

然后对结构体整体安排对齐:

为什么要进行整体对齐?

如果不进行此步,只进行内部对齐,那么考虑如下情形

image-20220503112100035

左右两种排列方法均满足结构体内对齐,对于右侧,e.from的起始地址是0x7fffffffdc85显然不满足int的对齐规则,而左侧经过整体对齐,e.from0x7fffffffdc84是可以整除4的,满足int的对齐规则

此时sizeof(e)=12,而不是1+4+4=9

整体对齐要求结构体的起始地址必须是最大成员大小的整数倍,在这里是int,4字节

因此结构体e的起始地址应当是能够整除4的并且距离rbp最近的并且能够放得下结构体e的地方,显然是0x7fffffffdc80

整体对齐只是考虑了最大成员大小的整数倍,一定能保证所有成员的对齐吗?

由于任意基本数据类型的大小都是2的幂次,因此最大成员的大小一定是任意成员大小的整数倍,那么起始地址是最大成员大小的整数倍的同时,也一定是任意成员大小的整数倍

#pragma pack(n)指定对齐

如果不写该语句则默认n=8字节对齐,所有对象的对齐方式改为:

地址值是\(min\{自己大小,n\}的整数倍\)

同样的程序在一开始时加入

1
#pragma pack(1)

指定一字节对齐,任意对象的地址是\(min\{自己大小,1\}的整数倍=1的整数倍\),即没有对齐

然后使用gdb调试观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p $rbp
$1 = (void *) 0x7fffffffe2f0
(gdb) p sizeof(e)
$2 = 9
(gdb) p &a
$3 = 0x7fffffffe2ef ""
(gdb) p &b
$4 = 0x7fffffffe2e5 ""
(gdb) p &e.name
$5 = 0x7fffffffe2e6 ""
(gdb) p &e.from
$6 = (int *) 0x7fffffffe2e7
(gdb) p &e.to
$7 = (int *) 0x7fffffffe2eb
image-20220503115539724

此时栈帧就非常紧凑了,从rbp向下顺次紧密排列

反汇编观察栈的分布

无对齐时的func函数的反汇编视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:0000000000001139 func            proc near               ; CODE XREF: main+9↓p
.text:0000000000001139
.text:0000000000001139 var_B = byte ptr -0Bh
.text:0000000000001139 var_A = byte ptr -0Ah ;var_A放在rbp-10
.text:0000000000001139 var_1 = byte ptr -1
.text:0000000000001139
.text:0000000000001139 ; __unwind {
.text:0000000000001139 push rbp
.text:000000000000113A mov rbp, rsp
.text:000000000000113D sub rsp, 10h ;此处申请了16字节的栈空间
.text:0000000000001141 lea rcx, [rbp+var_B]
.text:0000000000001145 lea rdx, [rbp+var_A]
.text:0000000000001149 lea rax, [rbp+var_1]
.text:000000000000114D mov rsi, rax
.text:0000000000001150 lea rax, format ; "%x,%x,%x"
.text:0000000000001157 mov rdi, rax ; format
.text:000000000000115A mov eax, 0
.text:000000000000115F call _printf
.text:0000000000001164 nop
.text:0000000000001165 leave
.text:0000000000001166 retn
.text:0000000000001166 ; } // starts at 1139
.text:0000000000001166 func endp

默认对齐时的func函数反汇编视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:0000000000001139 func            proc near               ; CODE XREF: main+9↓p
.text:0000000000001139
.text:0000000000001139 var_11 = byte ptr -11h
.text:0000000000001139 var_10 = byte ptr -10h ;var_10放在rbp-16
.text:0000000000001139 var_1 = byte ptr -1
.text:0000000000001139
.text:0000000000001139 ; __unwind {
.text:0000000000001139 push rbp
.text:000000000000113A mov rbp, rsp
.text:000000000000113D sub rsp, 20h ;此处申请了32字节的栈空间
.text:0000000000001141 lea rcx, [rbp+var_11]
.text:0000000000001145 lea rdx, [rbp+var_10]
.text:0000000000001149 lea rax, [rbp+var_1]
.text:000000000000114D mov rsi, rax
.text:0000000000001150 lea rax, format ; "%x,%x,%x"
.text:0000000000001157 mov rdi, rax ; format
.text:000000000000115A mov eax, 0
.text:000000000000115F call _printf
.text:0000000000001164 nop
.text:0000000000001165 leave
.text:0000000000001166 retn
.text:0000000000001166 ; } // starts at 1139
.text:0000000000001166 func endp

全局结构体

以网络流中可能会用到的边结构体举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct{
int from; //源
int to; //目的
long long dist;//距离
long long flow;//流量
}Edge;//边结构体

Edge e;
void init(){//初始化边e
e.from=1;
e.to=2;
e.dist=1e12;
e.flow=1e13;
}
int main(){
init();
}

ida观察带gdb调试信息的exe文件
1
2
gcc main.c -g -O0 -o main.exe
ida64 main.exe

init函数的反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0000000000401560 ; void __cdecl init()
.text:0000000000401560 public init
.text:0000000000401560 init proc near ; CODE XREF: main+D↓p
.text:0000000000401560 push rbp
.text:0000000000401561 mov rbp, rsp
.text:0000000000401564 lea rax, e ;把e的地址放在rax中
.text:000000000040156B mov dword ptr [rax], 1 ;把1放在rax指向地址的第一个字
.text:0000000000401571 lea rax, e
.text:0000000000401578 mov dword ptr [rax+4], 2 ;把2放在rax指向地址偏移4字节之后的字中
.text:000000000040157F lea rax, e
.text:0000000000401586 mov rdx, 0E8D4A51000h ;把1e12放在rdx中
.text:0000000000401590 mov [rax+8], rdx ;把rdx中的值放在rax+8位置,宽度以rdx的64位为准
.text:0000000000401594 lea rax, e
.text:000000000040159B mov rcx, 9184E72A000h ;把1e13放在rcx中
.text:00000000004015A5 mov [rax+10h], rcx ;把rcx中的值放在rax+10位置,64位宽
.text:00000000004015A9 nop
.text:00000000004015AA pop rbp
.text:00000000004015AB retn
.text:00000000004015AB init endp

值得庆幸的是e被ida识别为一个Edge结构体的对象,这是因为使用gcc -g命令,编译形成的exe带有gbd调试信息

可以看出使用e的基地址+成员的偏移量进行结构体成员访问

其中e在bss段:

1
2
3
4
5
6
.bss:0000000000407970                 public e
.bss:0000000000407970 ; Edge e
.bss:0000000000407970 e Edge <?> ; DATA XREF: init+4↑o
.bss:0000000000407970 ; init+11↑o ...
.bss:0000000000407988 public __native_startup_state
...

e在bss段的地址[0x407970,407987]共24字节

默认8字节对齐下,最大成员long long正好8字节,因此任何成员都是按照地址是自己大小倍数进行对齐的

1
2
3
4
5
6
typedef struct{
int from; //源
int to; //目的
long long dist;//距离
long long flow;//流量
}Edge;//边结构体

from和to恰好占用第一个8字节,然后dist占用第二个8字节,然后flow占用第三个8字节,合计24字节

双击Edge可以观察其Structures视图

1
2
3
4
5
6
7
00000000 Edge            struc ; (sizeof=0x18, align=0x8, copyof_102)	;大小0x18=24字节,8字节对齐
00000000 ; XREF: .bss:e/r
00000000 from dd ? ;dword双字类型
00000004 to dd ? ;dword双字类型
00000008 dist dq ? ;quad四字类型
00000010 flow dq ? ;quad四字类型
00000018 Edge ends
ida观察不带调试信息的exe
1
2
gcc main.c -O0 -o main.exe			;不使用-g
ida64 main.exe

init函数的反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000401560                 public init
.text:0000000000401560 init proc near ; CODE XREF: main+D↓p
.text:0000000000401560 push rbp
.text:0000000000401561 mov rbp, rsp
.text:0000000000401564 lea rax, e
.text:000000000040156B mov dword ptr [rax], 1
.text:0000000000401571 lea rax, e
.text:0000000000401578 mov dword ptr [rax+4], 2
.text:000000000040157F lea rax, e
.text:0000000000401586 mov rdx, 0E8D4A51000h
.text:0000000000401590 mov [rax+8], rdx
.text:0000000000401594 lea rax, e
.text:000000000040159B mov rcx, 9184E72A000h
.text:00000000004015A5 mov [rax+10h], rcx
.text:00000000004015A9 nop
.text:00000000004015AA pop rbp
.text:00000000004015AB retn
.text:00000000004015AB init endp

到此和刚才带有调试信息的情况几乎相同,但是当我们双击e试图观察e是个什么东西时

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
.bss:0000000000407970                 public e
.bss:0000000000407970 e db ? ; ; DATA XREF: init+4↑o
.bss:0000000000407970 ; init+11↑o ...
.bss:0000000000407971 db ? ;
.bss:0000000000407972 db ? ;
.bss:0000000000407973 db ? ;
.bss:0000000000407974 db ? ;
.bss:0000000000407975 db ? ;
.bss:0000000000407976 db ? ;
.bss:0000000000407977 db ? ;
.bss:0000000000407978 db ? ;
.bss:0000000000407979 db ? ;
.bss:000000000040797A db ? ;
.bss:000000000040797B db ? ;
.bss:000000000040797C db ? ;
.bss:000000000040797D db ? ;
.bss:000000000040797E db ? ;
.bss:000000000040797F db ? ;
.bss:0000000000407980 db ? ;
.bss:0000000000407981 db ? ;
.bss:0000000000407982 db ? ;
.bss:0000000000407983 db ? ;
.bss:0000000000407984 db ? ;
.bss:0000000000407985 db ? ;
.bss:0000000000407986 db ? ;
.bss:0000000000407987 db ? ;
.bss:0000000000407988 public __native_startup_state
...

e退化为bss段上的24个字节,看不出结构体的痕迹了,

但是至少我们可以知道有这么一个叫做e的集合,它管理着24个字节

此时的结构体更像是一个鱼龙混杂的数组,有着不同大小的元素

栈上结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct{
int from;
int to;
long long dist;
long long flow;
}Edge;
Edge newEdge(int u,int v,long long d,long long f){//获取新的边对象,功能模仿构造函数
Edge e;
e.from=u;
e.to=v;
e.dist=d;
e.flow=f;
return e;
}
int main(){
Edge e=newEdge(1,2,1e12,1e13);
}
带调试信息

newEdge函数的反汇编视图

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
.text:0000000000401560 ; Edge *__cdecl newEdge(Edge *__return_ptr __struct_ptr retstr, int u, int v, __int64 d, __int64 f)
.text:0000000000401560 public newEdge
.text:0000000000401560 newEdge proc near ; CODE XREF: main+38↓p
.text:0000000000401560
.text:0000000000401560 e = Edge ptr -20h ;可以识别出结构体e
.text:0000000000401560 arg_0 = qword ptr 10h ;蜜汁操作,多一个arg_0参数
.text:0000000000401560 u = dword ptr 18h ;由于带调试信息,ida认识四个参数名
.text:0000000000401560 v = dword ptr 20h
.text:0000000000401560 d = qword ptr 28h
.text:0000000000401560 f = qword ptr 30h
.text:0000000000401560
.text:0000000000401560 push rbp
.text:0000000000401561 mov rbp, rsp
.text:0000000000401564 sub rsp, 20h
.text:0000000000401568 mov [rbp+arg_0], rcx
.text:000000000040156C mov [rbp+u], edx
.text:000000000040156F mov [rbp+v], r8d
.text:0000000000401573 mov [rbp+d], r9
.text:0000000000401577 mov eax, [rbp+u]
.text:000000000040157A mov [rbp+e.from], eax
.text:000000000040157D mov eax, [rbp+v]
.text:0000000000401580 mov [rbp+e.to], eax
.text:0000000000401583 mov rax, [rbp+d]
.text:0000000000401587 mov [rbp+e.dist], rax
.text:000000000040158B mov rax, [rbp+f]
.text:000000000040158F mov [rbp+e.flow], rax
.text:0000000000401593 mov rcx, [rbp+arg_0]
.text:0000000000401597 mov rax, qword ptr [rbp+e.from]
.text:000000000040159B mov rdx, [rbp+e.dist]
.text:000000000040159F mov [rcx], rax
.text:00000000004015A2 mov [rcx+8], rdx
.text:00000000004015A6 mov rax, [rbp+e.flow]
.text:00000000004015AA mov [rcx+10h], rax
.text:00000000004015AE mov rax, [rbp+arg_0]
.text:00000000004015B2 add rsp, 20h
.text:00000000004015B6 pop rbp
.text:00000000004015B7 retn
.text:00000000004015B7 newEdge endp
不带调试信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct{
int from;
int to;
long long dist;
long long flow;
}Edge;
Edge newEdge(int u,int v,long long d,long long f){//获取新的边对象,功能模仿构造函数
Edge e;
e.from=u;
e.to=v;
e.dist=d;
e.flow=f;
return e;
}
int main(){
Edge e=newEdge(1,2,1e12,1e13);
}

main函数反汇编视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:00000000004015B8 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000004015B8 public main
.text:00000000004015B8 main proc near ; CODE XREF: __tmainCRTStartup+22F↑p
.text:00000000004015B8
.text:00000000004015B8 var_30 = qword ptr -30h
.text:00000000004015B8 var_20 = byte ptr -20h
.text:00000000004015B8
.text:00000000004015B8 push rbp
.text:00000000004015B9 mov rbp, rsp
.text:00000000004015BC sub rsp, 50h
.text:00000000004015C0 call __main
.text:00000000004015C5 lea rax, [rbp+var_20] ;&var_20->rax
.text:00000000004015C9 mov rdx, 9184E72A000h ;1e13->rdx
.text:00000000004015D3 mov [rsp+50h+var_30], rdx ;1e13->rdx->*(var_30+50h)
.text:00000000004015D8 mov r9, 0E8D4A51000h ;1e12->r9
.text:00000000004015E2 mov r8d, 2 ;2放在r8
.text:00000000004015E8 mov edx, 1 ;1放在rdx
.text:00000000004015ED mov rcx, rax ;rax是var_20的栈地址
.text:00000000004015F0 call newEdge
.text:00000000004015F5 mov eax, 0
.text:00000000004015FA add rsp, 50h
.text:00000000004015FE pop rbp
.text:00000000004015FF retn
.text:00000000004015FF main endp

xctf攻防世界-reverse-新手村

image-20220507171516740

002logmein

main函数

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
main proc near

var_90= qword ptr -90h;很奇怪的是,申请了好多变量,有四字的也有双字的还有字节的
var_88= qword ptr -88h
var_80= qword ptr -80h
var_74= dword ptr -74h
var_70= qword ptr -70h
var_64= dword ptr -64h
var_60= dword ptr -60h
var_5C= dword ptr -5Ch
var_57= byte ptr -57h
var_56= byte ptr -56h
var_55= byte ptr -55h
var_54= dword ptr -54h
s= byte ptr -50h
var_2C= dword ptr -2Ch
var_28= qword ptr -28h
var_20= byte ptr -20h
var_4= dword ptr -4

; __unwind {
;开端:
push rbp
mov rbp, rsp
sub rsp, 90h


mov rdi, offset format ; "Welcome to the RC3 secure password gues"...
mov [rbp+var_4], 0

;一系列蜜汁操作
mov rax, ds:qword_4008B0 ;0x5E54525F4C41223A;
mov qword ptr [rbp+var_20], rax
mov rax, ds:qword_4008B8 ;0x342F362B3F2E2A4C;
mov qword ptr [rbp+var_20+8], rax
mov cx, ds:word_4008C0 ;0x36;
mov word ptr [rbp+var_20+10h], cx

分析一下这里将数据传来传去干了啥

image-20220506112847647

var_20占据了栈上rbp-20到rbp-9共24字节空间

0x5E54525F4C41223A放在栈上var_20,占用四字8字节,

0x342F362B3F2E2A4C放在栈上var_20+8,恰好顺着刚才的var_20存放八个字节

0x36放在栈上var_20+10hvar_20+16也是顺着放的

到此为止,var_20一共使用了0~16共17字节,十六进制的表示为:0x36342F362B3F2E2A4C5E54525F4C41223A

表示成ascii码为:64/6+?.*L^TR_LA":,下面的字符啥也没写即为'\0'结束符号,一定要考虑清楚小端模式

由此可见var_20应当是开在栈上的一个字符数组,大小是24bytes,实际使用17bytes

然后

1
2
mov     rax, qword ptr ds:byte_4008D0			 ;0x65626D61726168;
mov [rbp+var_28], rax

0x65626D61726168一个四字放在栈中var_28上,var_28恰好也是8字节四字长度,表示成8个ascii字符为ebmarah刚好7个字符

然后

1
mov     [rbp+var_2C], 7

var_2C是个双字但是却只放了一个7,可能会与var_28长度7有染

接着下面的反汇编应该调整一下指令顺序,调用_printf或者_scanf函数返回后立刻处理eax中的返回值,看起来比较直观,并且不影响程序逻辑

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
;打印第一句废话
mov rdi, offset format ; "Welcome to the RC3 secure password gues"...
mov ...
call _printf ;printf函数返回值是实际打印字符数
mov [rbp+var_5C], eax
mov al, 0

;打印第二句废话
mov rdi, offset aToContinueYouM ; "To continue, you must enter the correct"...
call _printf
mov [rbp+var_60], eax
mov al, 0

;打印第三句废话
mov rdi, offset aEnterYourGuess ; "Enter your guess: "
call _printf
mov [rbp+var_64], eax
mov al, 0

;调用scanf获取输入
mov rdi, offset a32s ; "%32s"
lea rsi, [rbp+s] ; s作为scanf的缓冲区,获取输入
call ___isoc99_scanf ;scanf返回值是实际获取到的输入字符数
mov [rbp+var_74], eax ;输入字符数->eax

;获取输入长度
lea rdi, [rbp+var_20] ;&var_20->rdi
lea rsi, [rbp+s] ;&s->rsi
mov [rbp+var_70], rdi ;&var_20->rdi->var_70
mov rdi, rsi ;&s->rdi
call _strlen ;strlen(s)->rax
mov [rbp+var_80], rax ;strlen(s)->rax->var_80

;获取存好的字符串长度
mov rdi, [rbp+var_70] ;&var_20->var_70->rdi
call _strlen ;strlen(&var_20)->rax
mov rsi, [rbp+var_80] ;strlen(s)->var_80->rsi

;比较两个长度是否相同
cmp rsi, rax ;比较s和var_20的长度(17)是否一致,即判断是否输入了17个字符
jnb loc_400700 ;如果不一致则跳转报告失败,否则继续检查

下面即将进入循环,现在我们已知的是输入要17个字符

循环体分析

sub_4007c0函数报告失败,不妨给他起名failure

sub_4007F0函数报告成功,不妨给他起名success

image-20220506120601387

进入循环之前有一个var_54=0很自然可以想到的一种循环结构:

1
2
3
4
5
int i=0;
while(true){
...
++i;
}

那么这里var_54是循环变量i吗?

再看最下方的loc_40079E,其中对var_54进行了++,然后跳转loc_400707即循环开头,那么var_54基本上可以确定为循环变量i了

将反汇编翻译为伪代码

image-20220506124657651

解密算法已经很明显了,把var_20var_28都看成字符数组然后decrypt[i]=var_20[i]^var_28[i%7]即可

解密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <algorithm>
using namespace std;

string var_20 = "64/6+?.*L^TR_LA\":";
string var_28 = "ebmarah";

int main() {
reverse(var_20.begin(), var_20.end());//反转一下是因为string字符串最左面的字符是低位,而小端模式下最右边是低位
reverse(var_28.begin(), var_28.end());
for (int i = 0; i < var_20.size(); ++i) {
int temp = (int)var_20[i] ^ (int)var_28[i % 7];
cout << (char)temp;
}
return 0;
}

运行结果:

1
RC3-2016-XORISGUD

003insanity

信息收集

每次运行都根开盲盒似的,等待大约5秒之后给出一个结果

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
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
Your ability to hack is about as good as my ability to have free will.

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
#define YOU "massive failure"

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
I've got a good feeling about this one..... wait no. Maybe next time.

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
#define YOU "massive failure"

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
Your ability to hack is about as good as my ability to have free will.

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
rm -rf / : Permission denied

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
Your ability to hack is about as good as my ability to have free will.

静态分析

一张图截完,题啃腚不难

image-20220507122441995

实际上点进s或者任意一个存好的字符串到rodata区看一下就找到答案了

image-20220507171407247

下面分析一下程序逻辑

猜测是这样的:假设rodata区有n条语句组成一个数组,生成一个随机数然后对n取模,取该值对应下标的语句打印

main函数

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
; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near

argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h

; __unwind {
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov dword ptr [esp], offset s ; "Reticulating splines, please wait.." ;废话压栈
call _puts ;打印废话
mov dword ptr [esp], 5 ; seconds ;5秒压栈
call _sleep ;休眠5秒

;生成随机数种子
mov dword ptr [esp], 0 ; timer ;time(0)
call _time
mov [esp], eax ; seed ;time(0)的返回值作为参数
call _srand ;srand(time(0))

;获得一个随机数
call _rand ;rand()->eax
mov ecx, eax ;rand()->eax->ecx

;下面这一系列操作好迷啊
mov edx, 0CCCCCCCDh ;3435973837这个魔数是啥呢
mul edx ;eax*edx->edx:eax
shr edx, 3 ;edx/8
lea eax, [edx+edx*4] ;5edx->eax
add eax, eax ;10edx->eax
sub ecx, eax ;10edx
;分析了一阵子屁用没有

;取对应字符串并打印
mov eax, strs[ecx*4] ;ecx是下标,相邻亮指针偏移4字节
mov [esp], eax ; s
call _puts
xor eax, eax
leave
retn
; } // starts at 80483F0
main endp
image-20220507162916682

全篇没有看出一个取模来,难道是我老眼昏花了?

取模时的编译优化

这里应该就是取模,但是其实现好迷啊

1
2
3
4
5
6
7
8
9
10
call    _rand																;rand()->eax假设为16
mov ecx, eax ;rand()=16->eax->ecx

;下面这一系列操作好迷啊
mov edx, 0CCCCCCCDh ;3435973837这个魔数是啥呢
mul edx ;eax*edx->edx:eax=Ch:CCCCCCD0h
shr edx, 3 ;edx/8=C/8=1
lea eax, [edx+edx*4] ; 5->eax
add eax, eax ; 10->eax
sub ecx, eax ; 16-10=6

(5条消息) 逆向-取模运算_嘻嘻兮的博客-CSDN博客_取模的逆运算

image-20220507170255414

006re1

信息收集

1
2
3
4
5
6
PS C:\Users\86135\Desktop\xctf12\re1> ./re1   
欢迎来到DUTCTF呦
这是一道很可爱很简单的逆向题呦
输入flag吧:123456
flag不太对呦,再试试呗,加油呦
请按任意键继续. . .

输入flag之后必然会flag不对,然后程序阻塞,并且提示"请按任意键继续..."

这个题的图视图竟然可以用一个截屏截下来,必然简单

image-20220507101716875

逆向分析

开端

1
2
3
4
5
;开端
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 44h

防止栈缓冲区溢出

1
2
3
.text:00401006                 mov     eax, ___security_cookie
.text:0040100B xor eax, ebp
.text:0040100D mov [ebp+var_4], eax

装填flag,指令顺序有改动但是不影响逻辑

1
2
3
4
5
6
7
8
.text:00401018                 xor     eax, eax					;eax归零
.text:00401010 movdqu xmm0, ds:xmmword_413E34 ;3074656D30633165577B465443545544h
;即 0tem0c1eW{FTCTUD
.text:0040101F movdqu [ebp+var_44], xmm0 ;0tem0c1eW{FTCTUD->var_44
.text:00401024 mov [ebp+var_2C], eax ;eax=0->var_2C
.text:00401027 movq xmm0, ds:qword_413E44 ;7D465443545544h即 }FTCTUD
.text:0040102F movq [ebp+var_34], xmm0 ;}FTCTUD ->var_34
.text:00401034 mov [ebp+var_28], ax ;0->var_28

打印废话

1
2
3
4
5
6
.text:0040101A                 push    offset aDutctf  ; "欢迎来到DUTCTF呦\n"	;废话压栈作为参数
.text:00401038 call _printf ;打印第一句废话
.text:0040103D push offset asc_413E60 ; "这是一道很可爱很简单的逆向题呦\n"
.text:00401042 call _printf
.text:00401047 push offset aFlag ; "输入flag吧:"
.text:0040104C call _printf

获取输入,输入存储到var_24

1
2
3
4
.text:00401051                 lea     eax, [ebp+var_24]
.text:00401054 push eax
.text:00401055 push offset Format ; "%s"
.text:0040105A call _scanf

加载输入和flag

1
2
3
.text:0040105F                 add     esp, 14h
.text:00401062 lea eax, [ebp+var_24] ;输入字符串的地址
.text:00401065 lea ecx, [ebp+var_44] ;flag的地址

循环比较输入和flag,每次比较两个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00401068 loc_401068:                             ; CODE XREF: _main+82↓j
.text:00401068 mov dl, [ecx] ;flag[0]值放在dl
.text:0040106A cmp dl, [eax] ;比较flag[0]和输入值的大小
.text:0040106C jnz short loc_401088 ;如果不为零即输入和flag不相等则跳转,大概率跳转到寄
.text:0040106E test dl, dl ;dl中存放的是flag[i]如果为0说明遍历完毕,循环跳出
.text:00401070 jz short loc_401084 ;如果遍历完毕则跳转loc_401084
.text:00401072 mov dl, [ecx+1]
.text:00401075 cmp dl, [eax+1]
.text:00401078 jnz short loc_401088
.text:0040107A add ecx, 2
.text:0040107D add eax, 2
.text:00401080 test dl, dl
.text:00401082 jnz short loc_401068

两种情况,置eax为0或1然后合并判断

1
2
3
.text:00401084 loc_401084:                             ; CODE XREF: _main+70↑j
.text:00401084 xor eax, eax ;eax归零
.text:00401086 jmp short loc_40108D ;跳转loc_401108D
1
2
3
4
.text:00401088 loc_401088:                             ; CODE XREF: _main+6C↑j
.text:00401088 ; _main+78↑j
.text:00401088 sbb eax, eax ;带借位减法,eax=-1=0xFFFFFFFF
.text:0040108A or eax, 1 ;eax=1&0xFFFFFFF=1

合并.根据刚才的eax判断

1
2
3
4
5
6
7
8
.text:0040108D loc_40108D:                             ; CODE XREF: _main+86↑j
.text:0040108D test eax, eax ;判断eax是否为0
.text:0040108F jnz short loc_401098 ;如果eax!=0则跳转loc_401098
.text:00401091 push offset unk_413E90;flag get ;否则eax=0表示成功,"flag get"的地址压栈准备打印
.text:00401096 jmp short loc_40109D ;跳转loc_40109D
.text:00401098 loc_401098: ; CODE XREF: _main+8F↑j
.text:00401098 push offset aFlag_0 ; "flag不太对呦" ;"flag不太对呦"地址压栈,准备打印

尾声

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0040109D loc_40109D:                             ; CODE XREF: _main+96↑j
.text:0040109D call _printf ;打印刚才压入栈中的缓冲区
.text:004010A2 add esp, 4 ;退栈4
.text:004010A5 push offset Command ; "pause" ;"pause"串的地址压栈,准备为system函数传参
.text:004010AA call _system ;程序阻塞暂停

;以下部分可能是在检查栈缓冲区溢出?
.text:004010AF mov ecx, [ebp+var_4]
.text:004010B2 add esp, 4
.text:004010B5 xor ecx, ebp ; StackCookie
.text:004010B7 xor eax, eax
.text:004010B9 call @__security_check_cookie@4 ; __security_check_cookie(x)
.text:004010BE mov esp, ebp
.text:004010C0 pop ebp
.text:004010C1 retn
.text:004010C1 _main endp

参考微软官方文档

全局安全 Cookie 用于在使用 /GS(缓冲区安全检查)编译的代码中和使用异常处理的代码中提供缓冲区溢出保护。 进入受到溢出保护的函数时,Cookie 被置于堆栈之上;退出时,会将堆栈上的值与全局 Cookie 进行比较。 它们之间存在任何差异则表示已经发生缓冲区溢出,并导致该程序的立即终止。

通常, __security_init_cookie 在初始化时由 CRT 调用。 如果你跳过 CRT 初始化(例如,如果使用 /ENTRY 指定入口点),则必须自己调用 __security_init_cookie 。 如果 __security_init_cookie 不调用,全局安全 cookie 将设置为默认值,并且会危及缓冲区溢出保护。 由于攻击者可利用此默认 Cookie 值使缓冲区溢出检查无效,我们建议,在定义自己的入口点时,始终调用 __security_init_cookie

在进入任何受到溢出保护的函数前,必须调用 __security_init_cookie,否则将检测到虚假的缓冲区溢出。 有关详细信息,请参阅 C 运行时错误 R6035

007game

信息收集

一个32位exe程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                     |------------/ --------△--------|
|-----------------------●--------|
|------------/ --------◇--------|
|-----------------------■--------|
|--------------------|------------/ --------☆--------|
| |------------/ --------▽--------|
| |------------/ -----( ̄▽ ̄)/---|
| |--------------------(*°▽°)=3--|
二 |
| by 0x61 |
| |
|------------------------------------------------------|
input n,n(1-8)
1.△ 2.○ 3.◇ 4.□ 5.☆ 6.▽ 7.( ̄▽ ̄)/ 8.(;°Д°) 0.restart
n=

一个开灯和关灯的游戏,大意是:

有八栈灯编号为1到8,每次可以选择一盏灯改变其开关状态,并且其左右的灯也会改变开关状态,认为8号灯和1号灯也是相邻的

初始时八盏灯都是灭的

当八盏灯都亮起来的时候,就给flag

算法解决

首先考虑这个问题是否有解?

方便取模运算,将灯号从0编到7

灯号 0 1 2 3 4 5 6 7
初始 0 0 0 0 0 0 0 0
第一次 1 ==1== 1 0 0 0 0 0
第二次 1 0 ==0== 1 0 0 0 0
第三次 1 0 0 0 ==1== 1 0 0
第四次 1 0 0 0 0 ==0== 1 0
第五次 0 0 0 0 0 0 0 ==1==

经过五次变化之后,我们可以得到一栈两着的灯,并且其他暗着的灯我们可以视为一直暗着

==因此同时改变三栈灯的状态是可以推出等价于只改变一盏灯的状态==

从1号灯开始的变化导致了7号灯变化

同理可知从2号灯开始的变化导致0号灯变化

...

从i号灯开始的变化导致(i+6)%8的灯变化,其路径为i%8,(i+1)%8,(i+3)%8,(i+4)%8,(i+6)%8共五次变化

则8盏灯全部由暗变亮需要40次变化

可以编写解密脚本打印出这40个数字然后粘贴进game.exe解密

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <cstdio>
using namespace std;

void change(const int &i) {
cout << i % 8 + 1 << endl;//实际上还得是从1开始编号
cout << (i + 1) % 8 + 1 << endl;
cout << (i + 3) % 8 + 1 << endl;
cout << (i + 4) % 8 + 1 << endl;
cout << (i + 6) % 8 + 1 << endl;
}

int main() {
for (int i = 0; i < 8; ++i) {
change(i);
}
return 0;
}

结果粘贴进game.exe之后

1
done!!! the flag is zsctf{T9is_tOpic_1s_v5ry_int7resting_b6t_others_are_n0t}

静态分析

尝试1

假设我们不会玩这个游戏,但是可以猜测,这⑧盏灯是可以都点亮的,诚如是,则程序交出flag,退出

那么在反汇编图视图中应当有一些没有后继的叶子块,即成功的分支

满足条件的块只有一个,如图所示红色块

image-20220506234409960

但是这个块实际上干了和栈有关一些事,具体啥事也不想管了,总之这样猜测是错的

刚才用算法方法点亮了所有的灯,程序也给出了flag但是程序依然在执行

尝试2

每次输入改变灯状态之后会有一个对所有灯的状态检查,要么是在循环开始,要么是在循环尾部

这八盏灯的状态可能是在放在一个字节里的八位,通过按位操作改变状态

或者每个灯一个单元,放在一个数组里

观察反汇编的图视图,发现有这么八块基本相同的结构,大概率就是逐次判断灯的状态

image-20220507075557175

观察其中的一块

1
2
3
4
5
mov     eax, 1					;1->eax
shl eax, 0
movzx ecx, byte_532E28[eax] ;byte_532E28[1]->ecx
cmp ecx, 1 ;cmp byte_532E28[1],1
jnz short loc_45F671 ;不为0则表明灯灭,跳转从头再来

那么如果八块均不跳转jnz,那么紧接着就应该给出flag了

image-20220507080122539

结果该函数开头给出了好长一坨字符数组,然后给了这么一个循环,明显是有加密算法的

image-20220507081140177

蓝色块用作打印,和加密算法无关,暂时不关心

结尾loc_45EB61将var_94++,显然是作为循环变量的,

开头loc_45EB70var_940x38h=56进行比较,应该是判断循环结束条件,循环56次

loc_45EB70之前有一个mov [ebp+var_94],0显然是循环变量var_94一开始是下标0

那么不妨给var_94重命名i

循环体:

1
2
3
4
5
6
7
8
9
10
11
12
13
mov     eax, [ebp+i]
movsx ecx, [ebp+eax+var_44] ;var_44[i]->ecx
mov edx, [ebp+i]
movsx eax, [ebp+edx+var_88] ;var_88[i]->eax
xor eax, ecx ;var_44[i]^var_88[i]->eax
mov ecx, [ebp+i]
mov [ebp+ecx+var_88], al ;var_44[i]^var_88[i]->var_88[i]
mov eax, [ebp+i]
movsx ecx, [ebp+eax+var_88] ;var_88[i]->ecx
xor ecx, 13h ;var_88[i]^13h->ecx
mov edx, [ebp+i]
mov [ebp+edx+var_88], cl ;var_88[i]^13h->var_88[i]
jmp short loc_45EB61

这么一长段干了一件事 \[ var\_88[i]=var\_88[i]\oplus var\_44[i]\oplus 0x13 \] var_88在内存中占用-88到-50刚好0x38大小,与循环变量i的范围相同

同理var_44应当占用-44-C

在栈视图下编好数组

image-20220507083640431

回到反汇编视图

接下来考虑怎样把mov进入var_44数组的值提取出来放到一个数组里

image-20220507084508649

如果高亮.text:0045E975.text:0045EA55之后右键convert to C/C++ array(DWORD)

image-20220507084855688

得到的数组是这样的:

1
2
3
4
5
6
7
8
9
10
[+] Dump 0x45E975 - 0x45EA55 (224 bytes) :
unsigned int data[56] = {
0x12BC45C6, 0x40BD45C6, 0x62BE45C6, 0x05BF45C6, 0x02C045C6, 0x04C145C6, 0x06C245C6, 0x03C345C6,
0x06C445C6, 0x30C545C6, 0x31C645C6, 0x41C745C6, 0x20C845C6, 0x0CC945C6, 0x30CA45C6, 0x41CB45C6,
0x1FCC45C6, 0x4ECD45C6, 0x3ECE45C6, 0x20CF45C6, 0x31D045C6, 0x20D145C6, 0x01D245C6, 0x39D345C6,
0x60D445C6, 0x03D545C6, 0x15D645C6, 0x09D745C6, 0x04D845C6, 0x3ED945C6, 0x03DA45C6, 0x05DB45C6,
0x04DC45C6, 0x01DD45C6, 0x02DE45C6, 0x03DF45C6, 0x2CE045C6, 0x41E145C6, 0x4EE245C6, 0x20E345C6,
0x10E445C6, 0x61E545C6, 0x36E645C6, 0x10E745C6, 0x2CE845C6, 0x34E945C6, 0x20EA45C6, 0x40EB45C6,
0x59EC45C6, 0x2DED45C6, 0x20EE45C6, 0x41EF45C6, 0x0FF045C6, 0x22F145C6, 0x12F245C6, 0x10F345C6
};

实际上存储的是指令不是我们要的数值

但是可以发现,每个数据的高两个16进制位是我们要的数值

比如0x12BC45C6高两位那么将每个16进制数按位与0xFF000000即可只保留高两位,或者直接右移24位即可转化为低两位

但是对于var_88这个数组,它头几个元素赋值的指令占了7字节,可以手动抄到数组里

image-20220507090007954
1
2
3
4
5
6
7
8
9
unsigned int var_88_2[56] = {
0x7b000000, 0x20000000, 0x12000000, 0x62000000, 0x77000000, 0x6C000000, 0x41000000, 0x29000000,
0x7C8045C6, 0x508145C6, 0x7D8245C6, 0x268345C6, 0x7C8445C6, 0x6F8545C6, 0x4A8645C6, 0x318745C6,
0x538845C6, 0x6C8945C6, 0x5E8A45C6, 0x6C8B45C6, 0x548C45C6, 0x068D45C6, 0x608E45C6, 0x538F45C6,
0x2C9045C6, 0x799145C6, 0x689245C6, 0x6E9345C6, 0x209445C6, 0x5F9545C6, 0x759645C6, 0x659745C6,
0x639845C6, 0x7B9945C6, 0x7F9A45C6, 0x779B45C6, 0x609C45C6, 0x309D45C6, 0x6B9E45C6, 0x479F45C6,
0x5CA045C6, 0x1DA145C6, 0x51A245C6, 0x6BA345C6, 0x5AA445C6, 0x55A545C6, 0x40A645C6, 0x0CA745C6,
0x2BA845C6, 0x4CA945C6, 0x56AA45C6, 0x0DAB45C6, 0x72AC45C6, 0x01AD45C6, 0x75AE45C6, 0x7EAF45C6
};

解密脚本

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
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

unsigned int var_44[56] = {
0x12BC45C6, 0x40BD45C6, 0x62BE45C6, 0x05BF45C6, 0x02C045C6, 0x04C145C6, 0x06C245C6, 0x03C345C6,
0x06C445C6, 0x30C545C6, 0x31C645C6, 0x41C745C6, 0x20C845C6, 0x0CC945C6, 0x30CA45C6, 0x41CB45C6,
0x1FCC45C6, 0x4ECD45C6, 0x3ECE45C6, 0x20CF45C6, 0x31D045C6, 0x20D145C6, 0x01D245C6, 0x39D345C6,
0x60D445C6, 0x03D545C6, 0x15D645C6, 0x09D745C6, 0x04D845C6, 0x3ED945C6, 0x03DA45C6, 0x05DB45C6,
0x04DC45C6, 0x01DD45C6, 0x02DE45C6, 0x03DF45C6, 0x2CE045C6, 0x41E145C6, 0x4EE245C6, 0x20E345C6,
0x10E445C6, 0x61E545C6, 0x36E645C6, 0x10E745C6, 0x2CE845C6, 0x34E945C6, 0x20EA45C6, 0x40EB45C6,
0x59EC45C6, 0x2DED45C6, 0x20EE45C6, 0x41EF45C6, 0x0FF045C6, 0x22F145C6, 0x12F245C6, 0x10F345C6
};

unsigned int var_88[56] = {
0x7b000000, 0x20000000, 0x12000000, 0x62000000, 0x77000000, 0x6C000000, 0x41000000, 0x29000000,//手动补的
0x7C8045C6, 0x508145C6, 0x7D8245C6, 0x268345C6, 0x7C8445C6, 0x6F8545C6, 0x4A8645C6, 0x318745C6,
0x538845C6, 0x6C8945C6, 0x5E8A45C6, 0x6C8B45C6, 0x548C45C6, 0x068D45C6, 0x608E45C6, 0x538F45C6,
0x2C9045C6, 0x799145C6, 0x689245C6, 0x6E9345C6, 0x209445C6, 0x5F9545C6, 0x759645C6, 0x659745C6,
0x639845C6, 0x7B9945C6, 0x7F9A45C6, 0x779B45C6, 0x609C45C6, 0x309D45C6, 0x6B9E45C6, 0x479F45C6,
0x5CA045C6, 0x1DA145C6, 0x51A245C6, 0x6BA345C6, 0x5AA445C6, 0x55A545C6, 0x40A645C6, 0x0CA745C6,
0x2BA845C6, 0x4CA945C6, 0x56AA45C6, 0x0DAB45C6, 0x72AC45C6, 0x01AD45C6, 0x75AE45C6, 0x7EAF45C6
};


vector<char> preprocesser(const unsigned int arr[], int length) {
vector<char> v(length);
for (int i = 0; i < length; ++i) {
v[i] = arr[i] >> 24;
}
return v;
}


int main() {
vector<char> result = preprocesser(var_44, 56);
vector<char> key = preprocesser(var_88, 56);
for (int i = 0; i < 56; ++i) {
result[i] = result[i] ^ key[i] ^ 0x13;
cout << (char)result[i];
}

return 0;
}

运行结果:

1
zsctf{T9is_tOpic_1s_v5ry_int7resting_b6t_others_are_n0t}

008Helloc,CTF

没见过的指令

在分析之前,补几个指令

关于CSAPP上没有见过的指令

1.rep/repne

2.movsd/movsw/movsb

1
2
3
rep movsd               ; 没有见过的指令
movsw
movsb

查阅x86 - Assembly: REP MOVS mechanism - Stack Overflow

According to MSDN, "The instruction can be prefixed by REP to repeat the operation the number of times specified by the ecx register."

rep的操作数是一个指令,rep的作用是将该指令重复若干次,以ecx中的数字为重复次数,(每次ecx中的数字-1直到归零)

repne 当ecx!=0且ZF!=0,重复执行后边的指令,每执行一次ecx的值减1

image-20220506171358295

3.stosd/stosw/stosb

1
2
3
rep stosd
stosw
stosb

STOSB、STOSW 和 STOSD 指令分别将 AL/AX/EAX 的内容存入由 EDI 中偏移量指向的内存位置。

EDI 根据方向标志位的状态递增或递减。

4.scasd/scasw/scasb

SCASB、SCASW 和 SCASD 指令分别将 AL/AX/EAX 中的值与 EDI 寻址的一个字节 / 字 / 双字进行比较。

这些指令可用于在字符串或数组中寻找一个数值。

结合 REPE(或 REPZ)前缀,当 ECX > 0 且 AL/AX/EAX 的值等于内存中每个连续的值时,不断扫描字符串或数组。

收集信息

输入比较短的字符串会报告wrong!然后让重新输入,输入很长的字符串报告wrong!之后程序退出

image-20220506163947900

ida打开之后

main函数开端部分

1
2
3
4
...
mov esi, offset a437261636b4d65 ; "437261636b4d654a757374466f7246756e"
lea edi, [esp+70h+var_24]
...

这里有一串16进制编码,其他不管先解一下,CrackMeJustForFun"撬我只是为了娱乐",看上去是有一定语言意义的,作为flag交上去试试就对了

反汇编分析

下面分析一下程序都干了啥

开端

1
2
3
4
5
6
7
8
9
10
11
12
13
;开端
sub esp, 60h
mov ecx, 8
push ebx;寄存器值保存
push ebp
push esi
push edi

mov esi, offset a437261636b4d65 ; esi存放flag的地址
lea edi, [esp+70h+var_24] ; &var_24->edi
rep movsd ;ecx事先放好了8,这里重复执行movsd八次
movsw
movsb

这里

1
2
3
rep movsd  
movsw
movsb

三句话,让我蒙蔽了18分钟,我真是一个计组学的一塌糊涂的虚蛋

参考博客标志寄存器df_标志寄存器_shikaao14的博客-CSDN博客

这里干了一个字符串拷贝的事情,offset a437261636b4d65这个位置的字符串作为源,esp+70h+var_24这个位置的缓冲区作为目的进行拷贝.

为什么用到了三条指令?

源串:437261636b4d654a757374466f7246756e共占用了35字节 \[ 35\div 4=8··\ ·3 \] 首先rep movsd重复8次,每次拷贝4字节,一共拷贝了32字节,剩下了3个字节,拆成一个字用movsw然后最后一个字节用movsb

上述部分做的就是拷贝.data上的字符串到栈上var_24开始的35个字节

下面进入循环体

image-20220506181931623

外循环(外侧蓝线)

这个循环体的结构很容易猜想,收集信息时我们知道如果输入字符数比较少则程序会一直重复运行,不存在循环变量,终止条件之类.因此循环体完全可以按照单独一次执行进行饭分析

loc_40101A

这是循环体的起始部分

一些参数及其函数调用距离太远不直观,这里调整了一些语句位置,但是不改变程序逻辑

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
loc_40101A:
mov ecx, 8 ;rep的帮凶
xor eax, eax ;eax归零
lea edi, [esp+70h+var_48] ; edi指向了&var_48
rep stosd ;STOSB、STOSW 和 STOSD 指令分别将 AL/AX/EAX 的内容存入由 EDI 中偏移量指向的内存位置。
stosw
stosb
;eax被xor指令清零了,那么这里三条指令的作用是让var_48开始的35字节的栈空间置0

;打印第一句废话
push offset aPleaseInputYou ; "please input your serial:"
call _printf

;准备获取用户输入
lea eax, [esp+74h+var_5C] ;eax指向&var_5C
push eax ;&var_5C压栈作为scanf的缓冲区
push offset aS ; "%s"压栈作为第一个参数
call _scanf

;检查是否给了太多字符
lea edi, [esp+7Ch+var_5C] ; &var_5C->edi
or ecx, 0FFFFFFFFh ; ecx置全1
xor eax, eax ;eax归零,如果eax为0则ZF置零,eax存放scanf返回值,即实际输入字符数
add esp, 0Ch ;退栈12字节,不会修改ZF位
repne scasb ;ecx中全是1,相当于无限大,只要scanf有输入则repne成立;从var_5C指向的缓冲区中寻找0(eax中放的是0),每次ecx-1
not ecx ;ecx取反
dec ecx ;ecx-1
cmp ecx, 11h ;检查ecx和11h=17的大小

ja loc_40110D ;跳转则寄

xor ebx, ebx ;ebx寄存器归零,在loc_40101A的结尾干了这么一件事,不明觉厉,实际上是为后来的内圈循环初始化循环变量

检查输入字符数是怎样实现的?

1.第25行的scasb从edi指向的缓冲区检查,是否存在eax中的字符,因此之前(第21行)就安置好了edilea edi, [esp+7Ch+var_5C]

2.22行ecx置全1,需要减2^8次才能归零,因此该条件对第25行的repne指令没有限制作用

3.23行eax中是scanf的返回值,根据其值置ZF位(假设scanf获取到了输入,则eax不为0,则ZF为0),然后eax归零

4.24行退栈啥作用不知道,推测一开始预先多开了一些栈空间,可能是防止栈缓冲区溢出

5.25行repne成立的条件是ecx!=0,ZF!=1显然当scanf有获取到输入时是成立的,重复执行scasb,即在edi指向的&var_5C中寻找eax中存放的值0,即寻找var_5C的结束字符,每次ecx-1

6.26行ecx取反,然后第27行ecx-1,啥作用呢?

假设输入了ABC就三个字符,scanf返回后eax存3,var_5C={'A','B','C',0,0,...,0},

此时repne条件成立,在第四次检查时检查到0,目前的值为0xFFFFFFFB

然后ecx取反得到0b0100=4

然后ecx-1=3即输入字符数

即这两步执行完后ecx存放输入字符数

7.28行ecx和11h进行比较,如果ecx中的值大则跳转loc_40110D,即判断输入字符是否超过了11h=17个,

而我们一开始收集到的信息也是字符数很多时直接程序退出

8.loc_40110D作用是报告失败

loc_40101A的作用就是获取输入并检查输入长度是否超过17字节,超过则寄,没超过则进一步检查

loc_40105F(内侧绿线)

在研究该部分之前,先要弄明白sprintf函数

image-20220506184650282
1
int sprintf(char *str, const char *format, ...)

返回值是实际str接收到的字符数,将源缓冲区从基地址到'\0'以某种格式输出到str目的缓冲区

内圈循环

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
loc_40105F:
mov al, [esp+ebx+70h+var_5C] ;在进入循环体之前,loc_40101A最的最后,ebx置零,这里又和var_5C结合使用,可以大胆推测这是一个基址变址寻址,基址是esp+70h+var_5c即&var_5C,变址即偏移量ebx,以后都记作i
test al, al
jz short loc_4010B0 ;如果var_5c[i]为0即i遍历到var_5c字符串的末尾则跳转loc_4010B0



;如果jz条件跳转未实现则var_5C[i]!=0即不是字符串末尾
;以下将var_5C处的字符串转化成16进制格式的字符串然后放到var_48位置

;将var_5c[i]转化成16进制字符串,放到buffer
movsx ecx, al ;var_5c[i]->ecx ;字节拓展双字
push ecx ;var_5c[i]->ecx->压栈作为参数
lea edx, [esp+74h+Buffer] ;&Buffer->edx, ;Buffer是栈上两个字节
push offset Format ; "%x" ;%x压栈作为参数
push edx ; Buffer ;&Buffer->edx->压栈作为参数
call _sprintf ;sprintf(&Buffer,"%x",&var_5c[i]),返回值(写入字符总数)放eax
;这里sprintf函数把var_5c[i]以十六进制字符格式转换成字符串放到Buffer
;(Buffer两个字节,一个ascii码的16进制字符串也是两个字节)


lea edi, [esp+7Ch+Buffer] ;&Buffer->edi
or ecx, 0FFFFFFFFh ;ecx置全1
xor eax, eax ;根据eax置ZF,然后eax置零
add esp, 0Ch
repne scasb ;寻找buffer的结束位置
not ecx ;ecx存放buffer的结束字符下标
sub edi, ecx ;edi-ecx之后edi回退到Buffer起始位置


;下面加载var_48并确定其末尾位置
lea edx, [esp+70h+var_48] ;&var_48->edx, ;var_48是栈上一个32字节的缓冲区
mov ebp, ecx ;ebp暂时保存之前的ecx
or ecx, 0FFFFFFFFh

mov esi, edi ;edi存放Buffer基地址,放到esi
mov edi, edx ;edx存放var_48基地址,放到edi

repne scasb ;寻找edi中var_48串的结束字符位置
dec edi ;减1后edi指向var_48的最后一个字符

;这里看似是蜜汁操作,啥也没干,但是我感觉是因为没有用编译优化,编译器在将Buffer向var_48拷贝的时候,没有考虑Buffer的长度,而是假设Buffer是一个不知长度的字符串,先以最大效率双字拷贝,然后最后剩下的不足双字的1或者2或者3个字节使用字节拷贝
;这蜜汁操作想了一下午才想明白
mov ecx, ebp ;ebp把之前暂时保存的值还给ecx
shr ecx, 2 ;ecx之前存放buffer结束字符下标,现在/4必然是0
rep movsd ;按双字拷贝,但是Buffer长度/4=0,因此实际上这三句话啥也没干

mov ecx, ebp ;ebp把之前暂时保存的值还给ecx
and ecx, 3 ;ecx保留低2位(模4余数)
rep movsb ;Buffer按字节拷贝到var_48


inc ebx ;++i;
cmp ebx, 11h ;判断i是否遍历完了var_5C
jl short loc_40105F ;如果i<17则跳到本部分开头重新内圈循环,如果i>=17则意味着遍历完毕,跳出内圈循环

内圈循环做的就是将var_5c存放的串换成16进制格式,输出到var_48位置的串,以Buffer作为转换过渡

var_5c[i]--sprintf-->Buffer--strcat-->var_48

loc_4040B0
1
2
3
loc_4010B0:	
lea esi, [esp+70h+var_24] ;var_24事先放好了flag的16进制表示 &var_24->esi
lea eax, [esp+70h+var_48] ;var_48是刚才内圈循环时将输入转换成16进制串的表示 &var_28->eax

两个16进制数都放好了,可想而知下面要对两个数比较判断是否相同了,显然直接调用strcmp这种函数太过于明显,可能会采用逐个字节比较的方式

但是这样也比较容易发现,

当比较完了*var_24*var_48var_24[0]var_48[0]之后,如果要继续比较var_24[1]var_48[1],需要移动一下指针,即

1
2
inc esi;
inc eax;

这类的指令

loc_4010B8

按照刚才的猜想,这里应该有一个循环,实际上也是这样的

image-20220506205727285

我们很容易就在.text:004010D2找到了移动指针的语句,只不过是一次性移动两位,因为前面比较了两位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:004010B8 loc_4010B8:                             ; CODE XREF: _main+DA↓j
.text:004010B8 mov dl, [eax] ;解引用
.text:004010BA mov bl, [esi]
.text:004010BC mov cl, dl
.text:004010BE cmp dl, bl
.text:004010C0 jnz short loc_4010E0 ;这个跳转通向 寄 ,如果不让他跳则dl和bl要相同,即两个串对应字符相同
.text:004010C2 test cl, cl ;如果cl是0说明指针已经移动到空了,应当跳出循环
.text:004010C4 jz short loc_4010DC ;跳出循环,通向 成功
.text:004010C6 mov dl, [eax+1] ;一次性比较两个字符
.text:004010C9 mov bl, [esi+1]
.text:004010CC mov cl, dl
.text:004010CE cmp dl, bl
.text:004010D0 jnz short loc_4010E0 ;跳转就寄
.text:004010D2 add eax, 2 ;移动指针
.text:004010D5 add esi, 2
.text:004010D8 test cl, cl
.text:004010DA jnz short loc_4010B8 ;本次两个字符都相同,继续循环判断下两个字符是否相同
尾声

从刚才的循环出来就能够决定命运了,刚才的循环有两种跳转,一是loc_4010DC,另一个是loc_4010E0

其中loc_4010DC将eax置零

loc_4010E0将eax置-1

然后两路殊途同归到loc_4010E5

loc_4010E5是一个法官,只有eax值为0才让打印成功

如果eax值不为0则跳转loc_4010FB

image-20220506205845976

009opensource

给出的是一段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
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
if (argc != 4) {//要求argc=4,argv[0]是自带的程序位置,剩下即要输入3个参数
printf("what?\n");
exit(1);
}

unsigned int first = atoi(argv[1]);//第一个参数转化成字符串
if (first != 0xcafe) {//要求first=51966
printf("you are wrong, sorry.\n");
exit(2);
}

unsigned int second = atoi(argv[2]);//second为第二个参数转化成字符串
if (second % 5 == 3 || second % 17 != 8) {//要求second 模5不余三,模17余8
printf("ha, you won't get it!\n");
exit(3);
}

if (strcmp("h4cky0u", argv[3])) {//要求argv[3]="h4cky0u",长度为7
printf("so close, dude!\n");
exit(4);
}

printf("Brr wrrr grr\n");
//second%17=8,strlen(argv[3])=7
unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv[3]) - 1615810207;

printf("Get your key: ");
printf("%x\n", hash);
return 0;
}

三个参数可以为:

1
51966 25  h4cky0u

编译之后运行一下:

1
2
3
PS C:\Users\86135\Desktop\xctf12\opensource> ./opensource 51966 25  h4cky0u
Brr wrrr grr
Get your key: c0ffee

010no-strings-attached

尽量不依赖工具,比如ida-F5的伪代码,那么应该在以什么为基础进行分析呢?

objdump和ida的反汇编视图起码是可以的,没有必要和0和1组成的二进制码打交道,直接看反汇编即可

ida反汇编的图视图也是可以接受的,毕竟有了反汇编之后我们也可以轻松画出跳转关系和调用关系图,不妨让ida代劳了

ida-F5伪代码可以吗?

如果我能够轻松地将反汇编翻译成伪代码或者代码,那何乐而不为?但是我没这本事.

如果每个题上来就看伪代码对于从反汇编到伪代码的翻译是没有帮助的.

因此我觉得逆向分析应当基于反汇编和图视图,尽量少沉迷美色看伪代码

对于no-strings-attached这个ctf逆向题,前置知识:

x86汇编语言(如果不看伪代码的话)

==宽字符==

置换加密算法

ida的使用

main函数

main函数下调用了四个函数

image-20220504202713613

_setlocale是使用者的区域设定,比如时间,语言之类的,没有卵用

banner,prompt_authentication都是打印一些语句作为提示,也没有锤子用

authenticate关键在这里

authenticate函数

粗略浏览该函数,发现有一个call decrypt

image-20220504202933305

单就从decrypt,这种"解密"名字的函数就应该是关键函数,authenticate的其他部分先不管,直接看decrypt,

another(rename之后的名字)和s分别作为第二个和第一个参数传递给了decrypt函数

decrypt函数

image-20220504203108886

框框相连,转转不已,也不知道套了多少层循环了

首先按照CSAPP第三章的方法,将汇编翻译成带goto的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
char *decrypt(char *s,char *another){
int slength=strlen(s);
int anotherlength=strlen(another);
char *dest=(int *)malloc(slength+1);
strcpy(dest,s);
goto loc_80486F7;
loc_80486AF:
int var_18=0;
goto loc_80486E7;

loc_80486B8:
dest[var_1C]=dest[var_1C]-another[var_18];
++var_1C;
++var_18;

loc_80486E7://这一块的分析是比较繁琐的
if(var_18<anotherlength&&var_1C<slength){
goto loc_80486B8;
}

loc_80486F7:
if(var_1C>=slength){
return dest;
}
else{
goto loc_80486AF;
}
}

loc_80486E7的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:080486B8 loc_80486B8:                            ; CODE XREF: decrypt+9D↓j
.text:080486B8 mov eax, [ebp+var_1C]
.text:080486BB shl eax, 2
.text:080486BE add eax, [ebp+dest] ;dest+4*var_1C->eax
.text:080486C1 mov edx, [ebp+var_1C]
.text:080486C4 shl edx, 2
.text:080486C7 add edx, [ebp+dest] ;dest+4*var_1C->edx
.text:080486CA mov ecx, [edx] ;[dest+4*var_1C]->ecx
.text:080486CC mov edx, [ebp+var_18]
.text:080486CF shl edx, 2
.text:080486D2 add edx, [ebp+another] ;another+4*var_18->edx
.text:080486D5 mov edx, [edx] ;[another+4*var_18]->edx
.text:080486D7 mov ebx, ecx ;[dest+4*var_1C]->ebx
.text:080486D9 sub ebx, edx ;[dest+4*var_1C]-[another+4*var_18]->ebx
.text:080486DB mov edx, ebx ;[dest+4*var_1C]-[another+4*var_18]->ebx->dex
.text:080486DD mov [eax], edx ;[dest+4*var_1C]=[dest+4*var_1C]-[another+4*var_18]
.text:080486DF add [ebp+var_1C], 1 ;++var_1C
.text:080486E3 add [ebp+var_18], 1 ;++var_18

这么一长段用源代码表示就干了稀松的事情

1
2
3
4
loc_80486B8:
dest[var_1C]=dest[var_1C]-another[var_18];
++var_1C;
++var_18;

然后翻译成不带goto的c代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char *decrypt(char *s,char *another){
char *dest=(char *)malloc(strlen(s)+1);
strcpy(dest,s);
int var_18=0;
int var_1C=0;

while(var_1C<strlen(s)){
var_18=0;
while(var_18<strlen(another)&&var_1C<strlen(s)){
dest[var_1C]=dest[var_1C]-another[var_18];
++var_1C;
++var_18;
}
}
return dest;
}

这里两层while循环到底干了一个什么事呢?再进一步精简一下

1
2
3
4
5
6
7
8
9
10
char *decrypt(char *s,char *key){
char *dest=(char *)malloc(strlen(s)+1);
strcpy(dest,s);
int index=0;
for(int i=0;i<strlen(s);++i){
index=i%strlen(key);//计算s[i]应该减去的key数组中的哪一个元素
dest[i]-=another[index];//位移密码
}
return dest;
}

就是一个位移密码解密,s是需要解密的字符串,算法是用s 的每一个字符去减key的相应位置的字符,如果key不够长则key循环使用

回到authenticate函数

image-20220504201120562

这个函数还怪长的,不看F5伪代码,应当如何解读呢?

首先,在分析完decrypt函数之后,我们大体上可以知道,密文和密钥都是在.rodata只读区存好的,然后通过decrypt解密算法得到加密前的明文,下面要做的应该是获取键盘输入,然后将刚才的明文和键盘输入进行对比

至于为什么不直接存储明文然后和获取的键盘输入进行对比?这就好比给一个孩子一艘拼装好的乐高千年隼和一盒子千年隼零件的关系

考察孩子对加密算法的掌握,以及对加密算法汇编形式的掌握呗

然后,基于上述分析,可以推测authenticate函数要做的,大体可以分为四或者五个部分

1
2
3
4
5
6
0.开端(任何函数都有,通常是压栈保存调用者函数的帧指针rbp,申请函数栈,rbp作为当前函数帧指针)
1.解密
2.输入
3.处理输入(可以算是输入的一部分)
4.字符串对比
5.尾声(任何函数都有,通常是释放函数栈和退栈还原rbp为调用者函数的帧指针)

顺序不一定,但是也就是这几件事了,想不出来还能干什么

实际分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
;0.开端
.text:08048708 push ebp
.text:08048709 mov ebp, esp
.text:0804870B sub esp, 8028h

;1.解密获得字符串
.text:08048711 mov dword ptr [esp+4], offset another ; another表示密钥,其地址压栈作为第二个参数
.text:08048719 mov dword ptr [esp], offset s ; s ; s表示需要解密的字符串,地址压栈作为参数
.text:08048720 call decrypt ; 调用decrypt,他需要两个参数
.text:08048725 mov [ebp+s2], eax ; decrypt返回值->eax->s2

;2.键盘获得输入字符串
.text:08048728 mov eax, ds:stdin@@GLIBC_2_0 ;标准键盘输入
.text:0804872D mov [esp+8], eax ; stream ;stdin->eax->[esp+8]作为第三个参数
.text:08048731 mov dword ptr [esp+4], 2000h ; n ;2000h->[esp+4]作为第二个参数
.text:08048739 lea eax, [ebp+ws] ;ws->eax
.text:0804873F mov [esp], eax ; ws ;ws->eax->[esp]作为第一个参数
.text:08048742 call _fgetws ;fgetws函数共需要三个参数,都已经妥善安置入栈

;3.处理键盘输入
.text:08048747 test eax, eax ;判断是否读取到至少一个字符
;3.1如果没有获取到输入
.text:08048749 jz short loc_804879C ;啥也没读到,跳转loc_804879C尾声
;3.2如果获取到了输入
.text:0804874B lea eax, [ebp+ws] ;&ws->eax
.text:08048751 mov [esp], eax ; s ;&ws->eax->[esp]压栈作为参数
.text:08048754 call _wcslen ;&ws作为参数压栈,传递给_wcslen
.text:08048759 sub eax, 1 ;strlen(ws)-1->eax
.text:0804875C mov [ebp+eax*4+ws], 0 ;ws[strlen(ws)-1]='\0'

; 4.上述两个字符串比较
.text:08048767 mov eax, [ebp+s2] ;decrypt返回值->s2->eax
.text:0804876A mov [esp+4], eax ; s2 ;decrypt返回值压栈,作为第二个参数
.text:0804876E lea eax, [ebp+ws] ;&ws->eax
.text:08048774 mov [esp], eax ; s1 ;&ws->eax->[esp],作为第一个参数
.text:08048777 call _wcscmp ;类似strcmp,输入字符串和解密后字符串比较
.text:0804877C test eax, eax ;判断strcmp是否返回0
; 4.1如果不为0则两个字符串不相等
.text:0804877E jnz short loc_804878F ;如果不为0则跳转loc_804878F
; 4.2否哦则即0表明两个字符串相等
.text:08048780 mov eax, offset unk_8048B44 ;&"success welcome back"->eax
.text:08048785 mov [esp], eax ;"success"->[esp],作为参数
.text:08048788 call _wprintf ;打印
.text:0804878D jmp short loc_804879C ;跳转尾声
; 4.1续
.text:0804878F ; ---------------------------------------------------------------------------
.text:0804878F
.text:0804878F loc_804878F: ; CODE XREF: authenticate+76↑j
.text:0804878F mov eax, offset unk_8048BA4 ;&"Access denied"->eax
.text:08048794 mov [esp], eax ;eax->[esp]作为参数
.text:08048797 call _wprintf ;打印
.text:0804879C

;尾声
.text:0804879C loc_804879C: ; CODE XREF: authenticate+41↑j
.text:0804879C ; authenticate+85↑j
.text:0804879C mov eax, [ebp+s2]
.text:0804879F mov [esp], eax ; ptr
.text:080487A2 call _free
.text:080487A7 leave
.text:080487A8 retn
.text:080487A8 ; } // starts at 8048708
.text:080487A8 authenticate endp

分析到此算是了解了authenticate的逻辑,但是有几点是我想继续研究的

1.offset指令?

.text:08048711 mov dword ptr [esp+4], offset another ;这里offset的作用是什么?

能问出这种问题来属实是我计组学的太虚了

首先参考了博客汇编语言——转移指令(offset,jmp,jcxz) - 想54256 - 博客园 (cnblogs.com)

image-20220504204638327

又产生新问题,如果说mov si,offset s是将s的地址放到si寄存器中,那么为什么不用类似lea si,[s]的指令加载有效地址?

然后查阅了这篇博客汇编语言LEA和OFFSET区别_Baoli1008的博客-CSDN博客_lea与offset的区别

image-20220504204837123

数据定义伪指令:

数据定义伪指令

2.fgetws,fgets函数的区别?

参考fgets、fgetws | Microsoft Docs

相同点:

两个函数都有三个参数,从左向右分别为:数据存储位置,要读取的最大字符数,FILE结构体指针.

返回值都是实际读取到的字符数

不同点:

fgetwsfgets 的宽字符版本。

宽字符?

啥是宽字符?用多个字节来代表的字符称之为宽字符。

unicode是宽字符之一,但是已经实际上成为了宽字符的具体实现方法,windows上的c语言宽字符就是unicode,用两个字节表示一个字符

ASCII码表用一共字节表示一个字符,一个字节的值有2^8=256种即ASCII码最多有256个,对于键盘上出现的所有字符已经足够了

但是对于汉字,法语,日语等等各种语言,ASCII能表示的字符数真的太逊了,

而unicode码两个字节表示一个字符,两个字节的值有2^16>60000,

而相对比较复杂的汉字,大约就3000常用汉字,因此unicode是有能力表示地球上的各种字符的

image-20220504210716190

12345ABCDE一共10个字符,结尾还有一个'\0'字符,因此char cstr是11字节,wchar_t wcstr是22个字节

wchar_t类型的数组长度的函数不是strlen,是wcslen,有多少个有意义的字符(不包括结尾'\0')wcslen函数就返回多少

宽字符类型的字符串字面量前需要有L修饰,不写会报错

[错误] cannot initialize array of 'wchar_t' 从 a string literal with type array of 'char'

unicode部分码表:

类型 编码(十进制) 编码(十六进制)
小写字母(a~z) 97~122 61~7a
大写字母(A~Z) 65~90 41~5a
左右花括号{} {123,125} {7b,7d}
阿拉伯数字 48~57 30~39

3.事先存在的密文和密钥放在哪里?"Access denied"等字样又放在那里?

在authenticate函数里面双击s观察s的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.rodata:08048AA8 ; const wchar_t s
.rodata:08048AA8 s db 3Ah ; DATA XREF: authenticate+11↑o
.rodata:08048AA9 db 14h
.rodata:08048AAA db 0 ;蜜汁0
.rodata:08048AAB db 0 ;蜜汁0
.rodata:08048AAC dd 1436h
.rodata:08048AB0 db 37h ; 7
.rodata:08048AB1 db 14h
.rodata:08048AB2 db 0 ;蜜汁0
.rodata:08048AB3 db 0
.rodata:08048AB4 db 3Bh ; ;
.rodata:08048AB5 db 14h
.rodata:08048AB6 db 0
.rodata:08048AB7 db 0
.rodata:08048AB8 db 80h
.rodata:08048AB9 db 14h
.rodata:08048ABA db 0
.rodata:08048ABB db 0
...

.rodata是常量区,在程序运行之前,在编译时就能确定

s是宽字符数组,每个元素都是wchar_t类型,占两个字节,小端存储的话就可以写为:

1
0x1434 0000 0x1436 0000 0x1437....

奇怪的是为什么其中会有很多0?为什么字符不能够紧凑存放?

能问出这种问题来属实是我c基础太差了

image-20220504235300627

sizeof('a')sizeof(char)的结果竟然不一样

查阅资料,a字符常量,却以int四字节存储

因此对rodata段的s应该这样断句:

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
.rodata:08048AA8 s               db 3Ah                  ; DATA XREF: authenticate+11↑o
.rodata:08048AA9 db 14h
.rodata:08048AAA db 0
.rodata:08048AAB db 0

.rodata:08048AAC db 36h
.rodata:08048AAD db 14h
.rodata:08048AAE db 0
.rodata:08048AAF db 0

.rodata:08048AB0 db 37h ; 7
.rodata:08048AB1 db 14h
.rodata:08048AB2 db 0
.rodata:08048AB3 db 0

.rodata:08048AB4 db 3Bh ; ;
.rodata:08048AB5 db 14h
.rodata:08048AB6 db 0
.rodata:08048AB7 db 0

.rodata:08048AB8 db 80h
.rodata:08048AB9 db 14h
.rodata:08048ABA db 0
.rodata:08048ABB db 0

...

在IDA View视图上选中s的元素然后convert to array(DWORD)

image-20220504235955441
1
2
3
4
5
6
7
8
[+] Dump 0x8048AA8 - 0x8048B43 (155 bytes) :
unsigned int s[39] = {
0x0000143A, 0x00001436, 0x00001437, 0x0000143B, 0x00001480, 0x0000147A, 0x00001471, 0x00001478,
0x00001463, 0x00001466, 0x00001473, 0x00001467, 0x00001462, 0x00001465, 0x00001473, 0x00001460,
0x0000146B, 0x00001471, 0x00001478, 0x0000146A, 0x00001473, 0x00001470, 0x00001464, 0x00001478,
0x0000146E, 0x00001470, 0x00001470, 0x00001464, 0x00001470, 0x00001464, 0x0000146E, 0x0000147B,
0x00001476, 0x00001478, 0x0000146A, 0x00001473, 0x0000147B, 0x00001480, 0x00000000
};

就得到了s数组

同样的道理another字符数组也位于.rodata区

1
2
3
4
[+] Dump 0x8048A90 - 0x8048AA7 (23 bytes) :
unsigned int another[6] = {
0x00001401, 0x00001402, 0x00001403, 0x00001404, 0x00001405, 0x00000000
};

同样的道理,"Success..."等字样也位于.rodata区

image-20220505000407437

解密

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

unsigned int s[39] = {
0x0000143A, 0x00001436, 0x00001437, 0x0000143B, 0x00001480, 0x0000147A, 0x00001471, 0x00001478,
0x00001463, 0x00001466, 0x00001473, 0x00001467, 0x00001462, 0x00001465, 0x00001473, 0x00001460,
0x0000146B, 0x00001471, 0x00001478, 0x0000146A, 0x00001473, 0x00001470, 0x00001464, 0x00001478,
0x0000146E, 0x00001470, 0x00001470, 0x00001464, 0x00001470, 0x00001464, 0x0000146E, 0x0000147B,
0x00001476, 0x00001478, 0x0000146A, 0x00001473, 0x0000147B, 0x00001480, 0x00000000
};

unsigned int another[6] = {
0x00001401, 0x00001402, 0x00001403, 0x00001404, 0x00001405, 0x00000000
};

wchar_t ans[100];

int main() {
int index = 0;
for (int i = 0; i < 38; i++) {
index = i % 5;
s[i] -= another[index];
swprintf(&ans[i], L"%s", &s[i]);
}
printf("%ls", ans);
return 0;
}

运行结果:

1
9447{you_are_an_international_mystery}

011csaw2013reversing2

信息收集

运行程序之后直接弹窗,推测是一个win32API-messageBox

image-20220506092936536

上面都是写的乱码,下面三个选择框都导致程序结束

静态分析

critical

正常运行时执行左侧逻辑,但是左侧逻辑就是用win32API-messageBox显示了一些乱码,没有卵用

因此应该审查调试运行时的逻辑,应该只要是调试运行就可以执行sub_4001000但是没有打印输出,估计调试器可以观察出某些变量的值,

但是我目前只会用gdb调试器,但是这个题给出的文件明显编译时没有-g选项,没有调试信息

还是从静态分析入手,分析解密函数sub_401000

解密函数sub_401000

image-20220506095147714

解密算法就是把s字符数组四个字符按照小端规则看成一个双字去和双字类型的key=0xDDCCAABB按位异或,然后再将双字拆成四个ascii字符即得到flag

堆上的缓冲区s拷贝的是unk_409B10,将其转化为c双字数组

image-20220506095859599
1
2
3
4
unsigned int unk_409B10[9] = {
0xBCA0CCBB, 0xB8BED1DC, 0xAEBECFCD, 0x82ABC4D2, 0xB393D9D2, 0xA993DED4, 0x82B8CBD3, 0xB9BECBD3,
0x00CCD79A
};

解密程序:

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
#include <iostream>
#include <algorithm>
using namespace std;

union IntAndChar4 {
protected:
unsigned int inInt;//inInt和inChar[4]共用四个字节
unsigned char inChar[4];
public:
void setInt(const int &i) {
inInt = i;
}
int getInt()const {
return inInt;
}
string getString()const {
string s;
for (int i = 0; i < 4; ++i) {
s += inChar[i];
}
return s;
}
friend ostream &operator<<(ostream &os, const IntAndChar4 &iac4) {
os << iac4.getString();
return os;
}
} iac4[9];

unsigned int key = 0xDDCCAABB;
unsigned int encrypted[9] = {
0xBCA0CCBB, 0xB8BED1DC, 0xAEBECFCD, 0x82ABC4D2, 0xB393D9D2, 0xA993DED4, 0x82B8CBD3, 0xB9BECBD3,
0x00CCD79A
};

void decrypt() {
for (int i = 0; i < 9; ++i) {
iac4[i].setInt(key ^ encrypted[i]);
cout << iac4[i];
}
}


int main() {
decrypt();

return 0;
}

运行结果:

1
flag{reversing_is_not_that_hard!}

012maze

尽量不看源代码,用CSAPP第三章的方法,将汇编语言首先转化为带goto和label的伪代码,然后转为不带goto和label的伪代码

这个题的名字maze很有意思,迷宫,既体现在反汇编翻译成伪代码时分支众多,又体现在最后使用走迷宫的方法获得flag

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
.text:00000000004006B0 ; int __fastcall main(int, char **, char **)
;注意调用方式,所有局部变量的地址都基于栈顶指针rsp计算,不基于帧指针计算
.text:00000000004006B0 main proc near ; DATA XREF: start+1D↑o
.text:00000000004006B0
.text:00000000004006B0 var_28 = dword ptr -28h
.text:00000000004006B0 var_24 = dword ptr -24h
.text:00000000004006B0
.text:00000000004006B0 ; __unwind {

;开端,参数压栈,保存寄存器
.text:00000000004006B0 push rbp
.text:00000000004006B1 push r15
.text:00000000004006B3 push r14
.text:00000000004006B5 push rbx
.text:00000000004006B6 push rax

;栈上开了两个局部变量,var_24和var_28
.text:00000000004006B7 mov [rsp+28h+var_24], 0
.text:00000000004006BF mov [rsp+28h+var_28], 0

;提醒用户输入
.text:00000000004006C6 mov edi, offset s ; "Input flag:"
.text:00000000004006CB call _puts

;获取用户输入
.text:00000000004006D0 mov edi, offset format ; "%s"
.text:00000000004006D5 mov esi, offset s1;s1作为第一个参数传递给_scanf,作为缓冲区承载输入
.text:00000000004006DA xor eax, eax
.text:00000000004006DC call _scanf

;获取输入字符串的长度并与18h=24字节进行比较
.text:00000000004006E1 mov edi, offset s1 ; s
.text:00000000004006E6 call _strlen
.text:00000000004006EB mov rbx, rax
.text:00000000004006EE cmp rbx, 18h

;如果长度不够则跳转loc_400822报告失败
.text:00000000004006F2 jnz loc_400822

;否则即输入长度为18h=24字节,验证前五个字符和最后一个字符是否是nctf{balabala}这种结构
.text:00000000004006F8 mov edi, offset s1 ; s1
.text:00000000004006FD mov esi, offset s2 ; "nctf{"
.text:0000000000400702 mov edx, 5 ; n
.text:0000000000400707 call _strncmp
.text:000000000040070C test eax, eax
.text:000000000040070E jnz loc_400822
.text:0000000000400714 movzx eax, ds:byte_6010BF[rbx]
.text:000000000040071B cmp eax, 7Dh ; '}'
.text:000000000040071E jnz loc_400822

;设定循环变量初始值
.text:0000000000400724 mov edi, offset s1 ; s
.text:0000000000400729 call _strlen
.text:000000000040072E dec rax ;strlen(s1)-1->rax指向s1的最后一个非0字符的下标
.text:0000000000400731 mov ebx, 5 ;从5开始是由于0到4这前五个字符已经判断过是nctf{了,ebx将会作为循环变量i
.text:0000000000400736 cmp rax, 5 ;判断strlen(s1)-1和5的大小,即判断s1串除了nctf{}之外有无其他字符
.text:000000000040073A jbe loc_4007EE

;其他循环体变量初始化
.text:0000000000400740 lea r14, [rsp+28h+var_28];r14=&var_28,一定要分清r14里面放的是var_28的地址
.text:0000000000400744 lea r15, [rsp+28h+var_24];r25=&var_24
.text:0000000000400749 nop dword ptr [rax+00000000h];nop指令什么也不干
.text:0000000000400750

;下面进入循环体
;始终注意:
;r14=&var_28
;r15=&var_24
;rbx中放的是循环变量i
;s1是输入的字符串
;后来会用到的asc_601060是data区的一个全局字符数组,' ******* * **** * **** * *** *# *** *** *** *********'
;rbx作为循环变量,用来遍历s1字符数组,s1[rbx]相当于s1[i]
;后面专门摘出循环体进行分析
.text:0000000000400750 loc_400750: ; CODE XREF: main+133↓j
.text:0000000000400750 movsx eax, ds:s1[rbx]
.text:0000000000400757 xor ebp, ebp
.text:0000000000400759 cmp eax, 4Eh ; 'N'
.text:000000000040075C jg short loc_400780
.text:000000000040075E movzx eax, al
.text:0000000000400761 cmp eax, 2Eh ; '.'
.text:0000000000400764 jz short loc_4007A0
.text:0000000000400766 cmp eax, 30h ; '0'
.text:0000000000400769 jnz short loc_4007BB
.text:000000000040076B mov rdi, r14
.text:000000000040076E call sub_400680;小函数,作用是修改
.text:0000000000400773 jmp short loc_4007B8
.text:0000000000400773 ; ---------------------------------------------------------------------------
.text:0000000000400775 align 20h
.text:0000000000400780
.text:0000000000400780 loc_400780: ; CODE XREF: main+AC↑j
.text:0000000000400780 movzx eax, al
.text:0000000000400783 cmp eax, 4Fh ; 'O'
.text:0000000000400786 jz short loc_4007B0
.text:0000000000400788 cmp eax, 6Fh ; 'o'
.text:000000000040078B jnz short loc_4007BB
.text:000000000040078D mov rdi, r15
.text:0000000000400790 call sub_400660
.text:0000000000400795 jmp short loc_4007B8
.text:0000000000400795 ; ---------------------------------------------------------------------------
.text:0000000000400797 align 20h
.text:00000000004007A0
.text:00000000004007A0 loc_4007A0: ; CODE XREF: main+B4↑j
.text:00000000004007A0 mov rdi, r14
.text:00000000004007A3 call sub_400670
.text:00000000004007A8 jmp short loc_4007B8
.text:00000000004007A8 ; ---------------------------------------------------------------------------
.text:00000000004007AA align 10h
.text:00000000004007B0
.text:00000000004007B0 loc_4007B0: ; CODE XREF: main+D6↑j
.text:00000000004007B0 mov rdi, r15
.text:00000000004007B3 call sub_400650
.text:00000000004007B8
.text:00000000004007B8 loc_4007B8: ; CODE XREF: main+C3↑j
.text:00000000004007B8 ; main+E5↑j ...
.text:00000000004007B8 mov bpl, al
.text:00000000004007BB
.text:00000000004007BB loc_4007BB: ; CODE XREF: main+B9↑j
.text:00000000004007BB ; main+DB↑j
.text:00000000004007BB mov esi, [rsp+28h+var_24]
.text:00000000004007BF mov edx, [rsp+28h+var_28]
.text:00000000004007C2 mov edi, offset asc_601060 ; " ******* * **** * **** * *** *# "...
.text:00000000004007C7 call sub_400690
.text:00000000004007CC test al, al
.text:00000000004007CE jz short loc_400822;此处也可能出循环,当al即sub_400690返回值为0则跳出循环
.text:00000000004007D0 inc rbx
.text:00000000004007D3 mov edi, offset s1 ; s
.text:00000000004007D8 call _strlen
.text:00000000004007DD dec rax
.text:00000000004007E0 cmp rbx, rax;判断rbx是否遍历完了s1字符串
.text:00000000004007E3 jb loc_400750


;出了循环体,下面判断bpl是否为0
.text:00000000004007E9 test bpl, bpl
.text:00000000004007EC jz short loc_40080B

;检查出循环时,var_24+8*var_28是否等于23h=35字节,如果是则报告成功
.text:00000000004007EE
.text:00000000004007EE loc_4007EE: ; CODE XREF: main+8A↑j
.text:00000000004007EE movsxd rax, [rsp+28h+var_24]
.text:00000000004007F3 movsxd rcx, [rsp+28h+var_28]
.text:00000000004007F7 movzx eax, byte ptr asc_601060[rax+rcx*8] ; " ******* * **** * **** * *** *# "...
.text:00000000004007FF cmp eax, 23h ; '#'
.text:0000000000400802 jnz short loc_40080B

;置成功
.text:0000000000400804 mov edi, offset aCongratulation ; "Congratulations!"
.text:0000000000400809 jmp short loc_400810


;置失败
.text:000000000040080B ; ---------------------------------------------------------------------------
.text:000000000040080B
.text:000000000040080B loc_40080B: ; CODE XREF: main+13C↑j
.text:000000000040080B ; main+152↑j
.text:000000000040080B mov edi, offset aWrongFlag ; "Wrong flag!"


.text:0000000000400810
.text:0000000000400810 loc_400810: ; CODE XREF: main+159↑j
.text:0000000000400810 call _puts;打印刚才存放在edi中的字符串地址指向的字符串,可能是失败或成功

;函数尾声
.text:0000000000400815 xor eax, eax
.text:0000000000400817 add rsp, 8
.text:000000000040081B pop rbx
.text:000000000040081C pop r14
.text:000000000040081E pop r15
.text:0000000000400820 pop rbp
.text:0000000000400821 retn

;置失败
.text:0000000000400822 ; ---------------------------------------------------------------------------
.text:0000000000400822
.text:0000000000400822 loc_400822: ; CODE XREF: main+42↑j
.text:0000000000400822 ; main+5E↑j ...
.text:0000000000400822 mov edi, offset aWrongFlag ; "Wrong flag!"
.text:0000000000400827 call _puts
.text:000000000040082C mov edi, 0FFFFFFFFh ; status
.text:0000000000400831 call _exit
.text:0000000000400831 ; } // starts at 4006B0
.text:0000000000400831 main endp
.text:0000000000400831
.text:0000000000400831 ; ---------------------------------------------------------------------------
.text:0000000000400836 align 20h

循环体分析

循环体是本题关键

对于循环体,摘出来翻译成伪代码

其中一些sub_400680,sub_400670,sub_400660,sub_400650都是小函数,其作用是对var_24或var_28进行加一或者减一操作,然后根据他俩的值置bpl的值是否为0

image-20220506084842338

如果想要得到congratulations的结果,必须满足:

1.循环体每执行一遍,最后的时候都要满足asc_601060[var_24+var_28*8]==' '||asc_601060[var_24+var_28*8]=='#'

2.出循环的时候asc_601060[var_24+8*var_28]='#'var_24+8*var_28=36,并且bpl不能为0

问题转化

asc_601060=" ******* * **** * **** * *** *# *** *** *** *********"

其中空格字符的下标为0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54

如果要满足1的话,var_24+var_28*8=0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54

要满足2的话,最终结果var_24+var_28*8=36,bpl是一个循环中附带计算的值,现在不方便讨论其取值,

但是可以确定的是,如果出循环的时候var_24或者var_28有小于0或者大于8,则bpl一定为0,

那么可以粗略的认为var_24,var_28取值都在[0,8]之间(说粗略是因为有可能var_24,var_28在循环中可以越界但是后来又退进了[0,8])

转化成一维状态转移

将上述两点分析转化成一个深度优先搜索或者说动态规划的问题

var_24,var_28的值表示为(var_24,var_28)的数对(x,y),比如(1,2)就表示var_24=1,var_28=2,

一个数对与一个整数 建立映射关系(var_24,var_28)->var_24+8*var_28

初始时状态为(0,0)->0

结束时状态为(x,y)->36

中间的合法状态映射成的整数值有0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54

状态转移有四种情况: \[ (x,y)\rightarrow\begin{cases} (x-1,y)\\ (x+1,y)\\ (x,y-1)\\ (x,y+1)\\ \end{cases} \]

下面就找一条路径,使得这个映射值从0转移到36,中途的任何数对的映射值都应当落在合法值域中

可以写一深度优先搜索实现该问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

vector<int> legal = {0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54};

bool isLegal(const int &n) {//判断一个映射值是否输入legal合法映射值
for (auto i : legal) {
if (i == n)
return true;
}
return false;
}

bool visited[10][10];//记忆化搜索

struct Path {//记录中途经过状态的结构体
int x;
int y;
friend ostream &operator<<(ostream &os, const Path &p) {
os << "(" << p.x << "," << p.y << ")=" << p.x + p.y * 8;
return os;
}
void setCoordinate(const int &x, const int &y) {
this->x = x;
this->y = y;
}
} path[20];

char directors[20];//移动方向
bool DFS(const int &x, const int &y, int depth) {

if (x < 0 || x > 8 || y < 0 || y > 8)
return false;//粗略条件剪枝
if (visited[x][y])
return false;//记忆化搜索剪枝
visited[x][y] = true;//设置访问过
int sum = x + 8 * y;//计算映射值
if (isLegal(sum) == false)
return false;//不合法,剪枝
path[depth].setCoordinate(x, y);//合法,记录路径
if (sum == 36) {//判断映射值是否已经为终点值36
return true;
}

//向其他状态转移
directors[depth + 1] = 'o';//方向数组,记录转义方向
if (DFS(x + 1, y, depth + 1))
return true;

directors[depth + 1] = '0';
if (DFS(x, y + 1, depth + 1))
return true;

directors[depth + 1] = 'O';
if (DFS(x - 1, y, depth + 1))
return true;

directors[depth + 1] = '.';
if (DFS(x, y - 1, depth + 1))
return true;
return false;
}

int main() {
if (DFS(0, 0, 0)) {//从x=0,y=0,深度depth=0开始
//如果成功da则打印方向数组
for (int i = 1; i < 20; ++i) {
cout << directors[i];
}

}

return 0;
}

运行结果:

1
o0oo00O000oooo..OO

刚好18个字符,加上头尾的nctf{}之后

nctf{o0oo00O000oooo..OO}刚好24个字符,满足所有限制条件,因此得到了flag

转化为二维迷宫

做完了看了别人的writeup才恍然大悟

考虑为什么var_24+var_28*8这里有一个*8?

如果将asc_601060=" ******* * **** * **** * *** *# *** *** *** *********"以8字节为单位换行则得到:

1
2
3
4
5
6
7
8
  ******
* * *
*** * **
** * **
* *# *
** *** *
** *
********

星号,空格,井号的宽度不一样因此这里看上去对不齐

image-20220506091352319
image-20220506091523194

右下右右下下左下下下右右右右上上左左

翻译成oO0.四个字符就是o0oo00O000oooo..OO

同样可以得到flag

调用约定

为啥CSAPP第三章x86-64汇编学完了,但是看IDA的反汇编仍然是一头雾水?还得看一大堆东西,其中就有调用约定

为什么windows上和linux上,x86和x64上编译出来的代码有很多不同,为什么和CSAPP说的相差甚远?调用约定不同是一大原因

首先要说明的几点,也是实验中和查阅资料逐渐获得的几点

1.==各种调用约定是相对于x86而言的==,对x64无意义

The keywords _stdcall and _cdecl specify 32-bit calling conventions. That's why they are not relevant for 64-bit programs (i.e. x64). On x64, there is only the standard calling convention and the extended __vectorcall calling convenction.

来自stackoverflow

关键词_stdcall_cdecl特指32位的调用约定.64位上不一样,64位上只有标准调用约定,还有其拓展__vectorcall

即使在64位的函数前面用__cdecl或者__stdcall修饰,编译结果也是一样的

2.x86和x64汇编有较大出入,windows上和linux上的同一约定也有些许区别

x86上的调用约定

微软给出的==x86系统==上的调用约定:

image-20220427184436835

一定注意是x86系统上的,而我们现在的笔记本大多数都是x64系统了,会有一些出入

c调用约定__cdecl

C Declaration

1
<return_type> __cdecl <func_name>(para1,para2,...,paran);

对于x86系统,微软官方文档是这样写的:

x86

维基百科这样写的:

image-20220428152115878

在gcc编译的时候加入-m32选项即可使用32位编译,编译成x86系统的程序

test.c

1
2
3
4
5
6
7
8
9
int _cdecl func(int a,int b,int c,int d,int e,int f,int g,int h){
return a+b+c+d+e+f+g+h;
}
int _cdecl show(){
return func(1,2,3,4,5,6,7,8);
}
int _cdecl main(){
show();
}
1
gcc -O0 test.c -c -m32 -o test.o|objdump -d test.o > test.s|code test.s

使用-m32编译之后然后反汇编

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
test.o:     file format pe-i386


Disassembly of section .text:

00000000 <_func>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp ;蜜汁操作,有esp为啥还要获取一个ebp作为拷贝?
3: 8b 55 08 mov 0x8(%ebp),%edx
6: 8b 45 0c mov 0xc(%ebp),%eax
9: 01 c2 add %eax,%edx
b: 8b 45 10 mov 0x10(%ebp),%eax
e: 01 c2 add %eax,%edx
10: 8b 45 14 mov 0x14(%ebp),%eax
13: 01 c2 add %eax,%edx
15: 8b 45 18 mov 0x18(%ebp),%eax
18: 01 c2 add %eax,%edx
1a: 8b 45 1c mov 0x1c(%ebp),%eax
1d: 01 c2 add %eax,%edx
1f: 8b 45 20 mov 0x20(%ebp),%eax
22: 01 c2 add %eax,%edx
24: 8b 45 24 mov 0x24(%ebp),%eax
27: 01 d0 add %edx,%eax
29: 5d pop %ebp
2a: c3 ret

0000002b <_show>:
2b: 55 push %ebp
2c: 89 e5 mov %esp,%ebp
2e: 83 ec 20 sub $0x20,%esp ;申请0x20=32字节空间,刚好8个int参数×一个int是4个字节,但是蜜汁操作,为啥不用push逐次压栈,而是一次性申请空间
31: c7 44 24 1c 08 00 00 movl $0x8,0x1c(%esp)
38: 00
39: c7 44 24 18 07 00 00 movl $0x7,0x18(%esp) ;每个参数占用栈上4个字节,8个参数紧挨着
40: 00
41: c7 44 24 14 06 00 00 movl $0x6,0x14(%esp)
48: 00
49: c7 44 24 10 05 00 00 movl $0x5,0x10(%esp)
50: 00
51: c7 44 24 0c 04 00 00 movl $0x4,0xc(%esp)
58: 00
59: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
60: 00
61: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
68: 00
69: c7 04 24 01 00 00 00 movl $0x1,(%esp) ;栈顶一定存放的是最左侧的参数
70: e8 8b ff ff ff call 0 <_func>
75: c9 leave ;蜜汁指令,CSAPP上没有见过leave指令
76: c3 ret

00000077 <_main>:
77: 55 push %ebp
78: 89 e5 mov %esp,%ebp
7a: 83 e4 f0 and $0xfffffff0,%esp
7d: e8 00 00 00 00 call 82 <_main+0xb>
82: e8 a4 ff ff ff call 2b <_show>
87: b8 00 00 00 00 mov $0x0,%eax ;返回值放在eax,rax寄存器中
8c: c9 leave
8d: c3 ret
8e: 90 nop
8f: 90 nop

32位系统必定不会用到r开头的4字64位寄存器比如rax,rdx,rsp等等,最大用到e开头的寄存器,比如eax,esp

可以发现show函数在调用func函数,传参的时候没有用到一个寄存器,全都是用的堆栈,还可以发现函数名都是由下划线前缀的<_main>,<_func>,<_show>

在为函数参数申请栈空间的时候是一次性完成的,即有8个参数则直接在栈上申请0x20=32字节,然后分别用movl指令向栈上刚才申请的空间写入数据.

关于蜜汁操作参数的压栈方式,是一次性申请足够的空间然后mov还是逐次push?

stackoverflow上的说法:

  1. Why does x64 use mov rather than push? I assume it's just more efficient and wasn't available in x86.

That is not the reason. Both of these instructions also exist in x86 assembly language.

效率并且是否可实现不是原因.这两种指令(push和mov)在x86汇编语言中都存在

The reason why your compiler is not emitting a push instruction for the x64 code is probably because it must adjust the stack pointer directly anyway, in order to create 32 bytes of "shadow space" for the called function. See this link (which was provided by @NateEldredge) for further information on "shadow space".

编译器对x64不使用push指令的原因是:他需要直接调整栈顶指针,给前四个参数的压栈预留"影子空间"

x86不需要寄存器传递参数但是x64需要寄存器并且在被调用函数的一开始会把寄存器中的参数也压栈,那么这些寄存器中的参数将会压入影子空间.具体见后文的实验

关于蜜汁操作ebp(rbp)寄存器的作用:

行为:在每个函数开始时都会被压入栈中然后拷贝栈顶指针,在有些函数快要结束的时候又会从栈中获取先前压入栈中的值

比如一个典型的结构:

1
2
3
2b:	55                   	push   %ebp
2c: 89 e5 mov %esp,%ebp
2e: 83 ec 20 sub $0x20,%esp

查阅stackoverflow

image-20220427195841367

rbp is the frame pointer on x86_64. In your generated code, it gets a snapshot of the stack pointer (rsp) so that when adjustments are made to rsp (i.e. reserving space for local variables or pushing values on to the stack), local variables and function parameters are still accessible from a constant offset from rbp.

A lot of compilers offer frame pointer omission as an optimization option; this will make the generated assembly code access variables relative to rsp instead and free up rbp as another general purpose register for use in functions.

In the case of GCC, which I'm guessing you're using from the AT&T assembler syntax, that switch is -fomit-frame-pointer. Try compiling your code with that switch and see what assembly code you get. You will probably notice that when accessing values relative to rsp instead of rbp, the offset from the pointer varies throughout the function.

rbp是x86_64上的栈帧指针.在我们的代码中,rbp寄存器获取栈顶指针rsp的快照.

当rsp改变时(比如为局部变量预留空间或者通过push指令压栈),我们仍然可以通过使用rbp+偏移量这种方式调用上一个函数(或者说调用者)的局部变量或者函数参数.

很多编译器的优化,会不用上述方式(rbp+偏移量)调用上一个函数的局部变量或者函数参数,而是只用rsp+偏移量.然后省出rbp寄存器去干其他事.对于GCC编译器,使用-fomit-frame-pointer编译选项达到上述目的

按照我的理解,rbp的作用就是调用者的rsp副本,然后rsp为被调用者服务,rbp为调用者服务.

rbp只是在被调用者嗲用调用者的局部变量时,令寻址更方便,完全可以只用rsp达到目的

后来的实践证明我一开始的理解是错误的

rbp指向函数栈帧的高地址,即栈底,rsp指向函数栈帧的低地址,即栈顶

二者都是为当前函数服务的

函数的开端时会将上一个函数的rbp指针压栈保存,然后指向当前函数栈帧的栈底.函数尾声时会将上一个函数的rbp指针退栈还给rbp

1
gcc -O0 -fomit-frame-pointer test.c -c -m64 -o test.o|objdump -d test.o > test.s|code test.s
1
2
3
4
5
6
7
8
9
10
00000000000000d6 <main>:
d6: 48 83 ec 28 sub $0x28,%rsp
da: e8 00 00 00 00 callq df <main+0x9>
df: e8 ae ff ff ff callq 92 <show>
e4: b8 00 00 00 00 mov $0x0,%eax
e9: 48 83 c4 28 add $0x28,%rsp
ed: c3 retq
ee: 90 nop
ef: 90 nop

使用-fomit-frame-pointer编译选项之后确实ebp不踪影了

现在再看这个结构:

1
2
3
2b:	55                   	push   %ebp				;将上一个函数对上上个函数的ebp保存
2c: 89 e5 mov %esp,%ebp ;ebp获取上一个函数esp的副本
2e: 83 ec 20 sub $0x20,%esp ;esp为当前函数服务

最后将栈中刚才压入的ebp又还给ebp是还原上个函数对上上个函数的esp副本

关于蜜汁指令leave:

百度百科给出的解释:

image-20220427224030429

一定要注意,这里指令的源和目的操作数与我们通篇是相反的

这里百科给出的解释使用的是intel风格的汇编语言,mov 目的操作数,源操作数

寄存器前面有百分号的是AT&T风格的汇编语言,movq 源操作数,目的操作数

leave指令在AT&T风格下相当于:

1
2
movl %ebp,%esp
pop %ebp

而这刚好和每个函数一开始的

1
2
push   %ebp
mov %esp,%ebp

恰好相反

因此leave指令就是还原栈的一个过程

标准调用约定__stdcall

微软官方文档给出的解释:

The __stdcall calling convention is used to call Win32 API functions. The callee cleans the stack, so the compiler makes vararg functions __cdecl. Functions that use this calling convention require a function prototype. The __stdcall modifier is Microsoft-specific.

__stdcall用于修饰==Win32 API函数==.被调用者负责情理自己的函数栈,(因此编译器会把变参函数修饰为__cdecl(调用者清理栈容易实现变参)).使用__stdcall的函数需要一个函数原型(即接口)

1
return-type __stdcall function-name[( argument-list )]
Element Implementation
Argument-passing order
参数传递顺序
Right to left.
从右向左
Argument-passing convention
参数传递规则(值传递/引用传递)
By value, unless a pointer or reference type is passed.
除非参数是指针或者引用类型,否则采用值传递
Stack-maintenance responsibility
栈维护
Called function pops its own arguments from the stack.
被调用者自己清理自己用到的栈
Name-decoration convention
命名修饰规则
An underscore (_) is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list. Therefore, the function declared as int func( int a, double b ) is decorated as follows: _func@12
下划线开头,然后@,然后是十进制表示的参数表字节大小.
因此int func(int a,double b)将会被修饰为_func@12(int四个字节+double八个字节)
Case-translation convention
大小写转换规定
None
返回值位置 放在eax,rax寄存器中

用ida打开一个win32程序,其Winmain函数是这样分析的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00401000 ; int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
.text:00401000 __stdcall WinMain(x, x, x, x) proc near ; CODE XREF: start+C9↓p
.text:00401000
.text:00401000 hInstance = dword ptr 4
.text:00401000 hPrevInstance = dword ptr 8
.text:00401000 lpCmdLine = dword ptr 0Ch
.text:00401000 nShowCmd = dword ptr 10h
.text:00401000
.text:00401000 mov eax, [esp+hInstance]
.text:00401004 push 0 ; dwInitParam
.text:00401006 push offset DialogFunc ; lpDialogFunc
.text:0040100B push 0 ; hWndParent
.text:0040100D push 65h ; 'e' ; lpTemplateName
.text:0040100F push eax ; hInstance
.text:00401010 mov hInstance, eax
.text:00401015 call ds:DialogBoxParamA
.text:0040101B xor eax, eax
.text:0040101D retn 10h ;retn指令可以带参数
.text:0040101D __stdcall WinMain(x, x, x, x) endp

可以明显观察到,参数只使用栈传递,从右向左压栈,Winmain函数的栈帧:

image-20220428092006441

有一点与__cdecl不同的是retn 10h,并且貌似与官方文档不同的是,被调用者没有自己清理自己的堆栈,比如Winmain到结束了也没有看见退栈指令.

实际上这就是retn 10h要做的事情

10h=16字节然而四个参数刚好每个4字节,即retn XXh就是被调用者的退栈指令,和返回指令合并成一条指令了

如此减少了清理堆栈需要使用的指令

还是test.c

1
2
3
4
5
6
7
8
9
10
11
int _stdcall func(short a,short b,short c,short d,short e,short f,short g,short h){

return a+b+c+d+e+f+g+h;

}
int _stdcall show(){
return func(1,2,3,4,5,6,7,8);
}
int _stdcall main(){
show();
}

使用gcc,objdump,vscode素质三连

1
2
3
PS C:\Users\86135\Desktop\reverse\test_call> gcc test.c -O0 -m32 -c -o test.o
PS C:\Users\86135\Desktop\reverse\test_call> objdump test.o -d >test.s
PS C:\Users\86135\Desktop\reverse\test_call> code test.s

反汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

test.o: file format pe-i386


Disassembly of section .text:

00000000 <_func@32>: ;函数名<_func@32>下划线,@,参数表大小(单位:字节)
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 57 push %edi ;寄存器临时压栈保存,为后来的运算做准备,最后还要弹栈复原
4: 56 push %esi
5: 53 push %ebx
6: 83 ec 28 sub $0x28,%esp
9: 8b 45 08 mov 0x8(%ebp),%eax
c: 8b 4d 0c mov 0xc(%ebp),%ecx
f: 8b 5d 10 mov 0x10(%ebp),%ebx
12: 89 5d d0 mov %ebx,-0x30(%ebp)
15: 8b 75 14 mov 0x14(%ebp),%esi
18: 89 75 cc mov %esi,-0x34(%ebp)
1b: 8b 7d 18 mov 0x18(%ebp),%edi
1e: 8b 75 1c mov 0x1c(%ebp),%esi
21: 8b 5d 20 mov 0x20(%ebp),%ebx
24: 8b 55 24 mov 0x24(%ebp),%edx
27: 66 89 45 f0 mov %ax,-0x10(%ebp)
2b: 89 c8 mov %ecx,%eax
2d: 66 89 45 ec mov %ax,-0x14(%ebp)
31: 0f b7 45 d0 movzwl -0x30(%ebp),%eax
35: 66 89 45 e8 mov %ax,-0x18(%ebp)
39: 0f b7 45 cc movzwl -0x34(%ebp),%eax
3d: 66 89 45 e4 mov %ax,-0x1c(%ebp)
41: 89 f8 mov %edi,%eax
43: 66 89 45 e0 mov %ax,-0x20(%ebp)
47: 89 f0 mov %esi,%eax
49: 66 89 45 dc mov %ax,-0x24(%ebp)
4d: 89 d8 mov %ebx,%eax
4f: 66 89 45 d8 mov %ax,-0x28(%ebp)
53: 89 d0 mov %edx,%eax
55: 66 89 45 d4 mov %ax,-0x2c(%ebp)
59: 0f bf 55 f0 movswl -0x10(%ebp),%edx
5d: 0f bf 45 ec movswl -0x14(%ebp),%eax
61: 01 c2 add %eax,%edx
63: 0f bf 45 e8 movswl -0x18(%ebp),%eax
67: 01 c2 add %eax,%edx
69: 0f bf 45 e4 movswl -0x1c(%ebp),%eax
6d: 01 c2 add %eax,%edx
6f: 0f bf 45 e0 movswl -0x20(%ebp),%eax
73: 01 c2 add %eax,%edx
75: 0f bf 45 dc movswl -0x24(%ebp),%eax
79: 01 c2 add %eax,%edx
7b: 0f bf 45 d8 movswl -0x28(%ebp),%eax
7f: 01 c2 add %eax,%edx
81: 0f bf 45 d4 movswl -0x2c(%ebp),%eax
85: 01 d0 add %edx,%eax
87: 83 c4 28 add $0x28,%esp
8a: 5b pop %ebx ;对应函数开始时将寄存器压栈保存,现在退栈复原
8b: 5e pop %esi
8c: 5f pop %edi
8d: 5d pop %ebp
8e: c2 20 00 ret $0x20 ;被调用者自行清理自己的栈

00000091 <_show@0>:
91: 55 push %ebp
92: 89 e5 mov %esp,%ebp
94: 83 ec 20 sub $0x20,%esp ;一次性分配0x20=32字节空间然后使用mov指令将参数压栈
97: c7 44 24 1c 08 00 00 movl $0x8,0x1c(%esp)
9e: 00
9f: c7 44 24 18 07 00 00 movl $0x7,0x18(%esp)
a6: 00
a7: c7 44 24 14 06 00 00 movl $0x6,0x14(%esp)
ae: 00
af: c7 44 24 10 05 00 00 movl $0x5,0x10(%esp)
b6: 00
b7: c7 44 24 0c 04 00 00 movl $0x4,0xc(%esp)
be: 00
bf: c7 44 24 08 03 00 00 movl $0x3,0x8(%esp)
c6: 00
c7: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp)
ce: 00
cf: c7 04 24 01 00 00 00 movl $0x1,(%esp)
d6: e8 25 ff ff ff call 0 <_func@32>
db: 83 ec 20 sub $0x20,%esp
de: c9 leave
df: c3 ret

000000e0 <_main@0>:
e0: 55 push %ebp
e1: 89 e5 mov %esp,%ebp
e3: 83 e4 f0 and $0xfffffff0,%esp
e6: e8 00 00 00 00 call eb <_main@0+0xb>
eb: e8 a1 ff ff ff call 91 <_show@0>
f0: b8 00 00 00 00 mov $0x0,%eax
f5: c9 leave
f6: c3 ret
f7: 90 nop

<>上给出的建议

image-20220428092612932

微软__fastcall

<>是这样写的:

image-20220428154840944

微软官方文档:

image-20220428155346372

同样的程序,除了main函数之外,其他函数都用_fastcall修饰

1
2
3
4
5
6
7
8
9
10
11
int _fastcall func(short a,short b,short c,short d,short e,short f,short g,short h){

return a+b+c+d+e+f+g+h;

}
int _fastcall show(){
return func(1,2,3,4,5,6,7,8);
}
int main(){//如果main也用_fastcall修饰则报错没有入口点
show();
}

使用MSVC编译

1
2
3
4
5
6
7
8
9
10
C:\Users\86135\Desktop\reverse\test_call>cl test.c
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.29.30139 版
版权所有(C) Microsoft Corporation。保留所有权利。

test.c
Microsoft (R) Incremental Linker Version 14.29.30139.0
Copyright (C) Microsoft Corporation. All rights reserved.

/out:test.exe
test.obj

然后反编译

1
objdump test.obj -d >test.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

test.obj: file format pe-i386


Disassembly of section .text$mn:

00000000 <@func@32>: ;函数命名规则是@函数名@参数字节数
0: 55 push %ebp
1: 8b ec mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 66 89 55 f8 mov %dx,-0x8(%ebp)
a: 66 89 4d fc mov %cx,-0x4(%ebp)
e: 0f bf 45 fc movswl -0x4(%ebp),%eax
12: 0f bf 4d f8 movswl -0x8(%ebp),%ecx
16: 03 c1 add %ecx,%eax
18: 0f bf 55 08 movswl 0x8(%ebp),%edx
1c: 03 c2 add %edx,%eax
1e: 0f bf 4d 0c movswl 0xc(%ebp),%ecx
22: 03 c1 add %ecx,%eax
24: 0f bf 55 10 movswl 0x10(%ebp),%edx
28: 03 c2 add %edx,%eax
2a: 0f bf 4d 14 movswl 0x14(%ebp),%ecx
2e: 03 c1 add %ecx,%eax
30: 0f bf 55 18 movswl 0x18(%ebp),%edx
34: 03 c2 add %edx,%eax
36: 0f bf 4d 1c movswl 0x1c(%ebp),%ecx
3a: 03 c1 add %ecx,%eax
3c: 8b e5 mov %ebp,%esp
3e: 5d pop %ebp
3f: c2 18 00 ret $0x18 ;被调用者清理自己的栈
42: cc int3
43: cc int3
44: cc int3
45: cc int3
46: cc int3
47: cc int3
48: cc int3
49: cc int3
4a: cc int3
4b: cc int3
4c: cc int3
4d: cc int3
4e: cc int3
4f: cc int3

00000050 <@show@0>:
50: 55 push %ebp
51: 8b ec mov %esp,%ebp
53: 6a 08 push $0x8
55: 6a 07 push $0x7
57: 6a 06 push $0x6
59: 6a 05 push $0x5
5b: 6a 04 push $0x4
5d: 6a 03 push $0x3
5f: ba 02 00 00 00 mov $0x2,%edx ;顶多有两个参数放在寄存器传递,其余都用栈
64: b9 01 00 00 00 mov $0x1,%ecx
69: e8 00 00 00 00 call 6e <@show@0+0x1e>
6e: 5d pop %ebp
6f: c3 ret

00000070 <_main>:
70: 55 push %ebp
71: 8b ec mov %esp,%ebp
73: e8 00 00 00 00 call 78 <_main+0x8>
78: 33 c0 xor %eax,%eax
7a: 5d pop %ebp
7b: c3 ret

微软__thiscall

微软官方文档:

The Microsoft-specific __thiscall calling convention is used on C++ class member functions on the x86 architecture. It's the default calling convention used by member functions that don't use variable arguments (vararg functions).

微软特有的__thiscall调用约定用于x86体系上C++的成员函数.定参函数默认使用该种调用约定

Under __thiscall, the callee cleans the stack, which is impossible for vararg functions. Arguments are pushed on the stack from right to left. The this pointer is passed via register ECX, and not on the stack.

如果函数有__thiscall修饰则被调用者清理自己的栈,因此变参函数难以实现.

函数参数从右向左压栈.this指针通过ECX寄存器传递

On ARM, ARM64, and x64 machines, __thiscall is accepted and ignored by the compiler. That's because they use a register-based calling convention by default.

在ARM,ARM64还有x64机器上,__thiscall会被编译器直接忽略.因为编译器默认使用一种基于寄存器的调用约定

<>

image-20220428161940071

x64上的调用约定

Microsoft x64 calling convention

微软x64调用约定

The Microsoft x64 calling convention[18][19] is followed on Windows and pre-boot UEFI (for long mode on x86-64). The first four arguments are placed onto the registers. That means RCX, RDX, R8, R9 for integer, struct or pointer arguments (in that order), and XMM0, XMM1, XMM2, XMM3 for floating point arguments. Additional arguments are pushed onto the stack (right to left). Integer return values (similar to x86) are returned in RAX if 64 bits or less. Floating point return values are returned in XMM0. Parameters less than 64 bits long are not zero extended; the high bits are not zeroed.

微软x64调用约定适用于Windows和UEFI.

前四个参数,如果是整数或者结构体或者指针类型,则放在寄存器RCX,RDX,R8,R9寄存器里,如果是浮点数则放在XMM0到XMM3里

额为的参数放在栈里(从右向左压栈)

返回值如果小于等于64位则放在RAX寄存器里(类似于x86的情形)

浮点返回值放在XMM0里

小于64位的参数进行有符号拓展

Structs and unions with sizes that match integers are passed and returned as if they were integers. Otherwise they are replaced with a pointer when used as an argument. When an oversized struct return is needed, another pointer to a caller-provided space is prepended as the first argument, shifting all other arguments to the right by one place.[20]

结构体和联合体如果大小与整形匹配则被当作整形进行参数传递还有返回.否则,当他们作为参数时,会被一个指针替代

当需要一个超大的结构体需要返回时,指向调用方提供的空间的另一个指针将作为第一个参数,将所有其他参数向右移动一个位置

When compiling for the x64 architecture in a Windows context (whether using Microsoft or non-Microsoft tools), stdcall, thiscall, cdecl, and fastcall all resolve to using this convention.

不管使用的编译器是不是微软的工具,对于x64体系,stdcall,thiscall,cdecl,fastcall都会被忽略,然后使用上述方法处理

In the Microsoft x64 calling convention, it is the caller's responsibility to allocate 32 bytes of "shadow space" on the stack right before calling the function (regardless of the actual number of parameters used), and to pop the stack after the call. The shadow space is used to spill RCX, RDX, R8, and R9,[21] but must be made available to all functions, even those with fewer than four parameters.

在微软x64调用约定中,调用者在调用其他函数之前,有义务在栈上分配32字节的"影子空间",并且忽略实际上参数占用的大小,并且在调用结束后由调用者清理被调用者的堆栈.

影子空间的作用是用于将来存放RCX,RDX,R8,R9中的前四个参数,但是即使是没有不够四个参数的函数,也会预留一个32字节的影子空间

The registers RAX, RCX, RDX, R8, R9, R10, R11 are considered volatile (caller-saved).[22]

RAX, RCX, RDX, R8, R9, R10, R11这些寄存器都是volatile修饰的

image-20220428101247431

The registers RBX, RBP, RDI, RSI, RSP, R12, R13, R14, and R15 are considered nonvolatile (callee-saved).[22]

RBX, RBP, RDI, RSI, RSP, R12, R13, R14, and R15不用volatile修饰

For example, a function taking 5 integer arguments will take the first to fourth in registers, and the fifth will be pushed on top of the shadow space. So when the called function is entered, the stack will be composed of (in ascending order) the return address, followed by the shadow space (32 bytes) followed by the fifth parameter.

举个例子,一个有5参数的 函数,其前四个参数将会被放在寄存器里然后第五个参数竟会别压入栈顶,并且在影子空间之上.

因此当进入被调用函数时,栈中的组成按照从栈顶到栈底将是:返回值,影子空间,第五个参数

这里影子空间就是给前四个参数腾空,前四个参数使用寄存器传递之后在被调用者中会被重新压栈,即压入这个预留的影子空间

维基百科这样写的:

image-20220428152348161

x86 x64调用约定及传参顺序 - 一瓶怡宝 - 博客园 (cnblogs.com)

同样的程序test.c

1
2
3
4
5
6
7
8
9
int  func(int a,int b,int c,int d,int e,int f,int g,int h){
return a+b+c+d+e+f+g+h;
}
int show(){
return func(1,2,3,4,5,6,7,8);
}
int main(){
show();
}

使用如下命令gcc -O0 test.c -c -o test.o|objdump -d test.o > t.s|code t.s

首先不用编译优化,将test.c编译成目标文件test.o,

然后使用objdump反编译得到反汇编代码t.s

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
test.o:     file format pe-x86-64


Disassembly of section .text:

0000000000000000 <func>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 4d 10 mov %ecx,0x10(%rbp) ;蜜汁操作,将ecx中存放的参数也压入栈中
7: 89 55 18 mov %edx,0x18(%rbp)
a: 44 89 45 20 mov %r8d,0x20(%rbp)
e: 44 89 4d 28 mov %r9d,0x28(%rbp)
12: 8b 55 10 mov 0x10(%rbp),%edx
15: 8b 45 18 mov 0x18(%rbp),%eax
18: 01 c2 add %eax,%edx
1a: 8b 45 20 mov 0x20(%rbp),%eax
1d: 01 c2 add %eax,%edx
1f: 8b 45 28 mov 0x28(%rbp),%eax
22: 01 c2 add %eax,%edx
24: 8b 45 30 mov 0x30(%rbp),%eax
27: 01 c2 add %eax,%edx
29: 8b 45 38 mov 0x38(%rbp),%eax
2c: 01 c2 add %eax,%edx
2e: 8b 45 40 mov 0x40(%rbp),%eax
31: 01 c2 add %eax,%edx
33: 8b 45 48 mov 0x48(%rbp),%eax
36: 01 d0 add %edx,%eax
38: 5d pop %rbp
39: c3 retq

000000000000003a <show>:
3a: 55 push %rbp
3b: 48 89 e5 mov %rsp,%rbp
3e: 48 83 ec 40 sub $0x40,%rsp ;为子函数申请栈空间,但是蜜汁操作,8个int参数,一个int占4字节,理论上需要0x20=32字节空间,却申请了0x40=64字节的空间
42: c7 44 24 38 08 00 00 movl $0x8,0x38(%rsp) ;将立即数8放在栈中rsp+0x38位置
49: 00
4a: c7 44 24 30 07 00 00 movl $0x7,0x30(%rsp) ;将7放在栈中rsp+0x30位置
51: 00
52: c7 44 24 28 06 00 00 movl $0x6,0x28(%rsp) ;0x30-0x28=48-40=8,蜜汁操作,相邻两个参数在栈上距离8字节
59: 00
5a: c7 44 24 20 05 00 00 movl $0x5,0x20(%rsp)
61: 00
62: 41 b9 04 00 00 00 mov $0x4,%r9d ;立即数4放在r9d寄存器中
68: 41 b8 03 00 00 00 mov $0x3,%r8d
6e: ba 02 00 00 00 mov $0x2,%edx
73: b9 01 00 00 00 mov $0x1,%ecx ;立即数1放在ecx寄存器中
78: e8 83 ff ff ff callq 0 <func>
7d: 48 83 c4 40 add $0x40,%rsp
81: 5d pop %rbp
82: c3 retq

0000000000000083 <main>:
83: 55 push %rbp
84: 48 89 e5 mov %rsp,%rbp
87: 48 83 ec 20 sub $0x20,%rsp
8b: e8 00 00 00 00 callq 90 <main+0xd> ;蜜汁操作,90行就在下面,为啥要call一下
90: e8 a5 ff ff ff callq 3a <show>
95: b8 00 00 00 00 mov $0x0,%eax
9a: 48 83 c4 20 add $0x20,%rsp
9e: 5d pop %rbp
9f: c3 retq

1.函数名没有下划线前缀

2.show和main函数都有固定的格式:

1
2
3
4
5
6
7
8
push %rbp			;rbp是被调用者保存的寄存器,当前函数可以使用,但是最后结束的时候要还原rbp的状态,因此压栈存储先前状态
mov %rsp,%rbp ;将先前的栈顶指针存放在刚刚腾出空闲的rbp寄存器中
sub %0x..,%rsp ;栈顶指针下降,在栈上为将要调用的子函数申请栈空间
callq <..> ;调用函数
..;处理返回值 ;通常返回值在eax寄存器中,进行一些处理
add %0x..,%rsp ;子函数已经执行结束了,为其申请的栈帧不需要再存在了,复原栈顶指针位置
pop %rbp ;将被调用者有义务保存的寄存器rbp还原
retq ;本函数返回

3.关于show函数在调用具有8个参数的func函数时,参数如何安排

关于蜜汁操作参数安排

1.后面第5到8个参数使用栈传递,5位于0x20+rsp,8位于0x38+rsp,即约靠左的参数越靠近栈顶rsp

2.前面1到4个参数==使用寄存器传递==

3.在进入被调用者函数后,将刚才调用者通过寄存器传递的参数也放进栈里,

并且x64上调用者在为子函数申请栈空间的时候也会有意申请很大,为待会儿寄存器中的参数也压栈做准备

实际上这三条都完成之后和x86上的结果是相同的,

1
2
func(p1 ,p2 ,p3, p4 ,p5           ,...,plast	   );
func(ecx,edx,r8d,r9d,远离栈顶的地方,...,靠近栈顶的地方);

关于蜜汁操作四字节的int在栈上分配8字节空间:

在64位不管是windows还是linux系统上int都是4字节的,long long都是8字节的

上面这段程序中各个参数改成short,int,long,long long类型之后反编译得到的汇编语言,在为子函数申请栈空间的时候都是0x40=64个字节

即参数不管什么类型都是以8字节传递的,这一点可以从使用r9d寄存器传递int参数看出

1
2
62:	41 b9 04 00 00 00    	mov    $0x4,%r9d			;立即数4放在r9d寄存器中
68: 41 b8 03 00 00 00 mov $0x3,%r8d

r开头的寄存器都是4字寄存器,理论上是放long long 的,但是这里int也用了r9d传递

关于蜜汁操作就在下一行的指令还要call

案发现场:

1
2
3
4
5
...
8b: e8 00 00 00 00 callq 90 <main+0xd> ;蜜汁操作,90行就在下面,为啥要call一下
90: e8 a5 ff ff ff callq 3a <show>
95: b8 00 00 00 00 mov $0x0,%eax
...

写一个更短的程序观察这个事

test.c

1
2
3
4
void  foo(){}
int main(){
foo();
}
1
2
gcc test.c -O0 -c -o test.o|objdump -d test.o > test.s|code test.s
不开任何编译优化,反汇编

反编译得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 90 nop
5: 5d pop %rbp
6: c3 retq

0000000000000007 <main>:
7: 55 push %rbp
8: 48 89 e5 mov %rsp,%rbp
b: 48 83 ec 20 sub $0x20,%rsp
f: e8 00 00 00 00 callq 14 <main+0xd>
14: e8 e7 ff ff ff callq 0 <foo>
19: b8 00 00 00 00 mov $0x0,%eax
1e: 48 83 c4 20 add $0x20,%rsp
22: 5d pop %rbp
23: c3 retq
24: 90 nop
...

main+0xf处的callq,将下一条指令也就是main+0x14压栈,然后修改程序计数器为main+0xf,即执行jmp main+0xf

main+0x14处的callq,将下一条指令地址也就是main+0x19压栈,然后修改程序计数器为foo地址,即执行jmp foo

foo执行到最后有一个retq作用是将栈顶刚才压入的main+0x19还给程序计数器rip,然后退栈,即pop %rip

这样看起来程序已经出错了,栈顶还有一个main+0xf没有弹出,但是main+0x22处有一个退栈将位于栈顶main+0xf弹给了%rbp寄存器,然而实际上%rbp寄存器应当获取次栈顶的值,即在main+0x7压入的值

出错的原因是main+0xf处的call指令调用的不是一个函数,没有与该call指令相对应的ret指令,这导致了call前压栈但是call后不退栈.

下面正向编译观察这个事情

使用gcc -S选项正向编译成汇编语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
call show
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-1) 9.2.0"

第9行有一个call __main

stackoverflow上的说法

Calls the _main function which will do initializing stuff that gcc needs. Call will push the current instruction pointer on the stack and jump to the address of _main

调用__main函数,初始化gcc需要的材料.该调用将当前程序计数器压栈然后跳转__main函数

显然我们gcc -c生成的目标文件.o是没有__main函数的 ,该函数应当是链接阶段加上去的

那么我们编译成exe文件之后再反编译进行观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0000000000401633 <main>:
401633: 55 push %rbp
401634: 48 89 e5 mov %rsp,%rbp
401637: 48 83 ec 20 sub $0x20,%rsp
40163b: e8 c0 00 00 00 callq 401700 <__main> ;此call确实调用了__main函数
401640: e8 a5 ff ff ff callq 4015ea <show> ;此call调用了show函数
401645: b8 00 00 00 00 mov $0x0,%eax
40164a: 48 83 c4 20 add $0x20,%rsp
40164e: 5d pop %rbp
40164f: c3 retq
0000000000401700 <__main>:
401700: 8b 05 2a 59 00 00 mov 0x592a(%rip),%eax # 407030 <initialized>
401706: 85 c0 test %eax,%eax
401708: 74 06 je 401710 <__main+0x10>
40170a: c3 retq ;有ret语句
40170b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
401710: c7 05 16 59 00 00 01 movl $0x1,0x5916(%rip) # 407030 <initialized>
401717: 00 00 00
40171a: e9 71 ff ff ff jmpq 401690 <__do_global_ctors>
40171f: 90 nop

此时可以看到,两个call都是调用的函数,并且调用的函数都有ret语句与call匹配

还要补充的是关于对齐:申请栈空间时要按照16字节对齐申请

System V AMD64 ABI

image-20220428152425649

CSAPP写道,参数传递时可以用到六个寄存器,多余的参数用栈传递,是指在64位linux环境下,

而windows上只能用四个寄存器传递参数,多余的用栈传递

image-20220427223059552

还是刚才的c程序,在ubuntu上的情况

main.c

1
2
3
4
5
6
7
8
9
int _cdecl func(int a,int b,int c,int d,int e,int f,int g,int h){
return a+b+c+d+e+f+g+h;
}
int _cdecl show(){
return func(1,2,3,4,5,6,7,8);
}
int _cdecl main(){
show();
}

其反汇编代码

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
main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func>:
0: f3 0f 1e fa endbr64 ;蜜汁指令
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d fc mov %edi,-0x4(%rbp) ;寄存器传递的参数也压栈,这与windows上相同
b: 89 75 f8 mov %esi,-0x8(%rbp)
e: 89 55 f4 mov %edx,-0xc(%rbp)
11: 89 4d f0 mov %ecx,-0x10(%rbp)
14: 44 89 45 ec mov %r8d,-0x14(%rbp)
18: 44 89 4d e8 mov %r9d,-0x18(%rbp)
1c: 8b 55 fc mov -0x4(%rbp),%edx
1f: 8b 45 f8 mov -0x8(%rbp),%eax
22: 01 c2 add %eax,%edx
24: 8b 45 f4 mov -0xc(%rbp),%eax
27: 01 c2 add %eax,%edx
29: 8b 45 f0 mov -0x10(%rbp),%eax
2c: 01 c2 add %eax,%edx
2e: 8b 45 ec mov -0x14(%rbp),%eax
31: 01 c2 add %eax,%edx
33: 8b 45 e8 mov -0x18(%rbp),%eax
36: 01 c2 add %eax,%edx
38: 8b 45 10 mov 0x10(%rbp),%eax
3b: 01 c2 add %eax,%edx
3d: 8b 45 18 mov 0x18(%rbp),%eax
40: 01 d0 add %edx,%eax
42: 5d pop %rbp
43: c3 retq

0000000000000044 <show>:
44: f3 0f 1e fa endbr64
48: 55 push %rbp
49: 48 89 e5 mov %rsp,%rbp
4c: 6a 08 pushq $0x8
4e: 6a 07 pushq $0x7
50: 41 b9 06 00 00 00 mov $0x6,%r9d ;确实使用了6个寄存器传递参数
56: 41 b8 05 00 00 00 mov $0x5,%r8d
5c: b9 04 00 00 00 mov $0x4,%ecx
61: ba 03 00 00 00 mov $0x3,%edx
66: be 02 00 00 00 mov $0x2,%esi
6b: bf 01 00 00 00 mov $0x1,%edi
70: e8 00 00 00 00 callq 75 <show+0x31>
75: 48 83 c4 10 add $0x10,%rsp
79: c9 leaveq
7a: c3 retq

000000000000007b <main>:
7b: f3 0f 1e fa endbr64
7f: 55 push %rbp
80: 48 89 e5 mov %rsp,%rbp
83: b8 00 00 00 00 mov $0x0,%eax
88: e8 00 00 00 00 callq 8d <main+0x12>
8d: b8 00 00 00 00 mov $0x0,%eax
92: 5d pop %rbp
93: c3 retq
1
2
func(para1,para2,para3,para4,para5,para6,para7,...,paran)
func(edi,esi,edx,ecx,r8d,r9d,栈上远离栈顶,...,栈上靠近栈顶)

gbd调试器的使用

环境:Win11+Kali子系统

启动

启动gdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(root㉿Executor)-[/home/kali]
└─# gdb
GNU gdb (Debian 10.1-2+b1) 10.1.90.20210103-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)

看到命令提示符号变成(gdb)则启动成功

安静启动gdb -q

1
2
3
┌──(root㉿Executor)-[/home/kali]
└─# gdb --silent
(gdb)
1
--silent也可以写成-q,-quiet

分屏启动gdb -tui

1
2
┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb -tui
image-20220421202637748

上方窗口是源代码窗口,下方是gbd命令行窗口

这样启动不需要另开一个终端观察源代码

并且当程序在端点停下的时候上方窗口也会显示当前程序停止的位置

上方的源代码窗口使用上下箭头移动视野

分屏+安静+指定调试程序gdb -tui -q <prog_name>

注意使用gdb调试的文件必须是可执行文件(windows上的.exe或者linux上的.out等)

并且在编译该可执行文件的时候==必须加入-g选项==生成gbd调试信息

如果不使用-g生成了.out文件然后使用gdb调试则

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/home/kali/mydir]
└─# gcc main.c -Og -o main.out

┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb --silent main.out
Reading symbols from main.out...
(No debugging symbols found in main.out) #报告没有调试信息
(gdb)

使用-tui打开,源代码都看不到

image-20220421203357054
1
2
3
4
5
┌──(root㉿Executor)-[/home/kali/mydir]
└─# gcc main.c -Og -g -o main.out #使用-g选项生成调试信息

┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb -tui --silent main.out
image-20220421203550729

带参数启动--args <程序> <参数1> <参数2>...

1
2
┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb -q -tui --args main.out 1 2 3 4

这里指定的参数1 2 3 4将会作为main.out执行时的命令行参数

启动后运行前

加载需要调试的程序

当在命令行直接使用gdb命令打开gdb调试器时,此时是没有指定需要调试的程序的

工作目录pwd

默认工作目录是打开gdb的位置,gdb启动后也可以使用pwd命令观察当前工作目录

1
2
3
4
┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb -q
(gdb) pwd
Working directory /home/kali/mydir.

指定调试程序位置file <prog_name>

对于当前目录下的程序可以直接使用程序名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(root㉿Executor)-[/home/kali/mydir]
└─# ls -l|gdb -q
(gdb) Undefined command: "total". Try "help".
(gdb) Undefined command: "-rw-r--r--". Try "help".
(gdb) Undefined command: "-rwxr-xr-x". Try "help".
(gdb) Undefined command: "-rw-r--r--". Try "help".
(gdb) quit

┌──(root㉿Executor)-[/home/kali/mydir]
└─# ls -l
total 28
-rw-r--r-- 1 root root 139 Apr 21 20:23 main.c
-rwxr-xr-x 1 root root 17672 Apr 21 20:35 main.out
-rw-r--r-- 1 root root 25 Apr 21 20:38 r.txt

┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb -q
(gdb) file main.out
Reading symbols from main.out...
(gdb)

对于其他目录下的可以使用绝对或者相对位置

1
2
3
(gdb) file /home/kali/mydir/main.out
Load new symbol table from "/home/kali/mydir/main.out"? (y or n) y
Reading symbols from /home/kali/mydir/main.out...

查看信息

查看当前工作目录pwd

1
2
(gdb) pwd
Working directory /home/kali/mydir.

查看是否找到目标程序文件list

1
2
3
4
5
6
7
8
9
10
11
(gdb) list
1 #include <stdio.h>
2 #include <stdlib.h>
3
4
5 int main(int argc,char **argv){
6 for(int i=0;i<argc;++i){
7 printf("%s",argv[i]);
8 }
9 return 0;
10 }

查看调试程序语言show language

1
2
(gdb) show language
The current source language is "auto; currently c".

查看源文件信息info source

1
2
3
4
5
6
7
8
9
(gdb) info source
Current source file is main.c
Compilation directory is /home/kali/mydir
Located in /home/kali/mydir/main.c
Contains 10 lines.
Source language is c.
Producer is GNU C17 11.2.0 -mtune=generic -march=x86-64 -g -Og -fasynchronous-unwind-tables.
Compiled with DWARF 2 debugging format.
Does not include preprocessor macro info.

查看可以设置的程序语言set language

1
2
(gdb) set language
Requires an argument. Valid arguments are auto, local, unknown, ada, asm, c, c++, d, fortran, go, minimal, modula-2, objective-c, opencl, pascal, rust.

查看程序运行状态info program

1
2
(gdb) info prog
The program being debugged is not being run.

设置信息

设置命令行参数set args <参数1> <参数2>...

1
2
3
(gdb) set args 1 2 3
(gdb) show args
Argument list to give program being debugged when it is started is "1 2 3".

如果在启动时有指定参数,此时再用set指定参数则会覆盖启动时设置的参数

设置语言'set language <语言>'

1
(gdb) set language c

运行

运行程序run

命令行参数使用启动时指定的参数或者set args设置的参数,如果都没有给定则无参数执行

如果有断点则程序在第一个断点处停止,否则直接运行完.

带参数运行run <参数1> <参数2>...

此参数将会直接作为运行参数,覆盖前面设置的参数

main停止运行start

start相当于在main函数处下了断点然后run,自动在main开始前停下

运行时

断点

设置断点b <行号>

断点可以运行前设置也可以运行时设置

1
2
(gdb) b 6
Breakpoint 6 at 0x555555555142: file main.c, line 6.

如果以-tui分屏打开,则设置好的断点会显示在行号左侧,大写的B+>意味当前程序暂停的断点

image-20220421213142314
b <函数名>直接给函数下断点
1
2
(gdb) b main
Breakpoint 10 at 0x555555555139: file main.c, line 5.

删除断点 delete <断点编号>

注意端点编号不是行号

删除全部断点则不指定编号,直接delete

删除指定行上的断点clear <行号>

条件断点b if <条件>

比如如果没有输入命令行参数时才给main函数下断点

1
2
(gdb) b main if argc==1					#用户没有输入时argc=1,第一个参数是当前程序位置
Breakpoint 11 at 0x555555555139: file main.c, line 5.

查看断点信息info b <断点号>

1
2
3
4
5
Breakpoint 11 at 0x555555555139: file main.c, line 5.
(gdb) info b 11
Num Type Disp Enb Address What
11 breakpoint keep y 0x0000555555555139 in main at main.c:5
stop only if argc==1
info b查看所有断点信息
1
2
3
4
5
6
(gdb) info b
Num Type Disp Enb Address What
11 breakpoint keep y 0x0000555555555139 in main at main.c:5
stop only if argc==1
12 breakpoint keep y 0x0000555555555142 in main at main.c:6
13 breakpoint keep y 0x0000555555555149 in main at main.c:7

查看信息

print命令

查看函数信息p <函数名>

函数信息也可以在运行前查看

1
2
(gdb) p main
$6 = {int (int, char **)} 0x555555555139 <main>
1
{返回值类型(参数1类型,参数2类型)} 函数地址 <函数名>
1
2
3
4
(gdb) whatis main
type = int (int, char **)
(gdb) ptype main
type = int (int, char **)
查看变量信息p <变量名>

查看变量信息必须是程序在该变量下文的断点处停下

即当前程序的运行位置必须已经经过变量,并且变量没有消亡

比如函数中的局部变量在函数返回之后就会消亡,只能在函数中断点然后查看断点之前的变量

如图调试一个用循环计算阶乘的函数,将断点下在第10行result*=n

image-20220422175357420

当程序第一次执行到次时会停在result*=n==执行前==的状态

image-20220422175511240

如图第一次在第10行停下,打印result=1

查看寄存器信息p $<寄存器名>

对于刚才的fact循环求阶乘函数,最后返回值是result,可想而知,该值是存放在rax寄存器中的

1
2
3
4
5
6
7
8
9
10
11
int fact(int n){
if(n<0)return n;
if(n==0)return 1;
int result=1;
while(n>0){
result*=n;
--n;

}
return result;
}

下面调试程序验证猜想

image-20220422180106540

还是将断点下到第10行while循环中

逐次进行循环,观察rax寄存器中的值

image-20220422180159934

与result的变化是一致的

也可以查看程序计数器rip中的值,观察程序当前进行位置

image-20220422180413841

用objdump反编译然后观察fact+13处的指令

image-20220422180540637

fact+13=0x1139+0x13=0x114c,该位置是一个test %edi,%edi指令,而n作为第一个参数是存放在edi寄存器中的

image-20220422180821161

紧接着114e处jg 1146意味如果R[%edi]=n>0则跳转1146位置,

而1146位置在114e上方,相当于跳进了循环,

也就是说0x114c处相当于循环判断while(n>0)

即程序在第10行的断点停下时rip中是第9行中的条件判断指令

x/<大小> <位置>检查字节或者字

x/20b fact检查fact函数的前20个字节
image-20220422181438589

与objdump得到的反汇编是一样的

x/2g 0x555555555139 检查从0x555555555139地址开始的双字
image-20220422181756825

info命令

查看所有寄存器信息info registers
image-20220422181858252

其中rax存放result,rdi存放n

查看栈帧'info frame'
image-20220422182024087

disas命令

反汇编当前程序暂停的函数disas

首先要在函数里下断点,然后程序在该断点暂停时使用disas可以观察当前函数的反汇编信息

image-20220422182255390

可见反汇编信息中也会有当前断点位置信息

如此就不用再开一个终端使用objdump观察了

反汇编指定名称的函数disas <函数名>

此方法不需要在函数中下断点

1
(gdb) disas fact
image-20220422182507141
反汇编某个地址附近的函数disas <地址>
1
disas 0x000055555555514c
image-20220422182706003

继续执行

执行有多种情况,通常会与断点或者一些逻辑结构联合使用,

比如在断点处停下或者不停下

在循环处,在函数中都有特殊的命令决定如何执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
源代码层面
next 单步执行,不进入函数,但是函数会执行然后返回值
next n 单步执行n行,均不进入函数

step 单步执行,进入函数
step n 单步执行n行,均进入函数

continue 恢复执行,直到预见下一个断点
continue n 恢复执行,并忽略下面的n个断点

finish 一直运行直到当前函数返回后停止,忽略断点

return 放弃后面的执行直接return


机器码层面(或者说汇编代码层面)
stepi 单步执行一条指令,进入函数
stepi n

nexti 单步执行一条指令,不进入函数(不会call)
nexti n

until 一直运行当前循环,在出循环之后的第一条语句停下,如果循环内有断点则在断点停下
实际上在机器码层面上,一直运行直到一个内存地址比当前更大的指令处停下

源代码层面

以一个递归求阶乘的程序为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(root㉿Executor)-[/home/kali/mydir]
└─# cat main.c
#include <stdio.h>
#include <stdlib.h>


int fact(int n){
if(n<0)return n;
if(n==0)return 1;
return n*fact(n-1);
}



int main(int argc,char **argv){
int ans=123456;
ans=fact(9);
printf("%d",ans);


return 0;
}
next单步步过

在main函数处下断点,使得程序上来先停一下,让我们有机会一行一行地执行

1
2
(gdb) b main
Breakpoint 3 at 0x55555555515a: file main.c, line 13.
image-20220422185956239

第一个n命令使得程序断在15:ans=fact(123456)

第二个n命令使得程序断在16:printf("%d",ans)

此时使用print命令观察ans的值发现其确实是9的阶乘,即第15行是自动执行然后返回了值的,单步步过只是忽略了执行细节,只要函数的执行后果

step单步步入

还是在main函数处下断点,然后使用step

image-20220422190333043

第一个step命令会让程序断在15:ans=fact(9)这与next是相同的

但是下一个step会进入step并在都5行停下

continue执行到下一个断点

在main函数开始(line 13)和main函数中打印ans前(line16)各打一个断点

image-20220422190554335

run运行之后会在13行停下,然后在输入c命令则会直接在16行停下

image-20220422190646915

此时print命令打印ans值发现为362880确实是9的阶乘,即两个断点之间的所有程序都被执行过了

finish一直运行到当前函数返回

分两种情况,有没有进入函数

使用step命令让程序在第一层递归函数==入口前==停下,此时使用finish

image-20220422191432439

发现程序直接返回到了main函数中,并且带着返回值362880恰好是9的阶乘,说明递归函数各层都执行了

现在让程序在第一层递归函数的==内部==停下

image-20220422192218738

发现进入的递归函数的第二层

return 放弃函数未执行的部分,直接返回到调用者
image-20220422192641695

fact(6)返回到fact(7)

然后一直使用finish命令返回main函数

image-20220422192818544

发现ans值并没有被正确地计算

即return会放弃下文

机器码层面

until 在循环体的机器码的最高地址时挑出循环到第一条高于循环地址的指令

调试一个使用循环计算阶乘的函数fact

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
┌──(root㉿Executor)-[/home/kali/mydir]
└─# cat main.c
#include <stdio.h>
#include <stdlib.h>


int fact(int n){
if(n<0)return n;
if(n==0)return 1;

int result=1;

while(n>0){
result*=n;
--n;

}
return result;
}



int main(int argc,char **argv){
int ans=123456;
ans=fact(9);
printf("%d",ans);


return 0;
}

在main函数打一个断点方便单步调试

一直使用step单步步入命令,直到第一次到达while条件判断的时候,使用disas观察反汇编代码

image-20220422194448277

此时对应指令fact+19位置

此时再使用一次单步步入,进入循环,line 12:result*=n

image-20220422194622378

对应指令fact+13位置

即源代码的执行顺序和机器码相反

显然是由于刚才的fact+21的jg条件跳转满足,跳到了fact+13

此时使用until只是相当于step命令,因为until只会在循环的机器码层面的最大地址处才会有快速执行循环的作用

image-20220422194902282

继续disas观察反汇编发现line13:--n对应反汇编的fact+16

此时再使用u

image-20220422195028273

源代码层面进行循环条件判断,对应机器码层面test判断,而fact+19就是循环体在机器层面的最高地址

此时再使用u

image-20220422195116698

直接返回了main函数,这是因为fact函数中while循环结束立刻就返回了,对应机器码

image-20220422195301219

第23行是循环外首条高于循环的地址,该条指令又是返回,因此返回了main函数

返回main后打印ans值发现是362880是9的阶乘,证明until指令会执行循环体剩下的部分

抽象代数

参考教材:密码编码学与网络安全

AES加密的数学基础

第一遍读感觉教材上对数学基础(群环域,多项式运算)P72-P91给的莫名奇妙

然后参考了知乎回答对群环域的解释

此时第二遍读教材,发现其实教材在讲每一部分的开始都写了为什么要讲.

学习这部分一定要明确目的,在陷入定理的证明时或者公式应用时时刻想想是在干什么

引入多项式构造\(GF(p^n)\)这一想法真的太神奇了,将整数上的数论定理应用于多项式也真的太神奇了

另,教材P74页的图4.2是有错误的

抽象代数基础

图片来自代数结构入门:群、环、域、向量空间 - 知乎 (zhihu.com)

代数结构入门:群、环、域、向量空间

\(<G,·>\)表示一个定义了二元关系\(·\)的集合(注意这里\(·\)不一定是乘号,可以是所有二元关系的抽象表示)

如果G满足:

A1封闭性:\(\forall a,b\in G\rightarrow a·b\in b\)

A2结合律:\(\forall a,b\in G,a·(b·c)=(a·b)·c\)

A3单位元:\(\exist e\in G,\forall a\in G,e·a=a·e=a\)

A4逆元:\(\forall a\in G,\exist a^{-1},a·a^{-1}=a^{-1}·a=e\)

这里\(^{-1}\)不是一定是-1次幂,只是逆元的表示形式

集合G+A1+A2+A3+A4=群G

<Z,+>就是一个群

<N,+>就不是一个群,因为如果定义单位元是0,那么1的逆元就是-1,但是-1不在自然数范围内,满足A1A2A3但是不满足A4的代数系统被称为幺半群

变换群

设A是一非空集合,G是A到A的映射集合,如果G关于运算*构成一个群,则称\(<G,✳>\)是集合A上的一个变换群,简称变换群

置换群

设A是一非空有限集合,则A上的一个变换群就是A的一个置换群,简称置换群

交换群

A5交换律:\(\forall a,b\in G,a·b=b·a\)

群G+A5=交换群G

l

\(<R,+,\times>\)R是一个有两种二元运算的集合,这两种二元运算分别为加法\(+\)和乘法\(\times\)

已知\(<R,+>\)是交换群

如果R再满足

M1乘法封闭性:\(\forall a,b\in R,a\times b\in R\)

M2乘法结合律:\(\forall a,b,c\in R,a\times (b\times c)=(a\times b)\times c\)

M3乘法对加法的分配律:\(\forall a,b,c\in R,\begin{cases}a\times (b+c)=a\times b+a\times c\\(a+b)\times c=a\times c+b\times c\end{cases}\)

注意这里没有写\(a \times (b+c)=(b+c)\times a\)

因为这样写实际上是满足乘法交换律,而满足分配律不一定满足交换律,比如矩阵乘法

矩阵的左右乘结果一般是不一样的,但是矩阵乘法对矩阵加法是有结合律的

则称R为环

交换环

如果环R再满足

M4乘法交换律:\(\forall a,b,c\in R,(a\times b)\times c=a\times (b\times c)\)

显然\(R_n(Q)\)是环但不是交换环

则称R为交换环

整环

如果交换环R再满足

M5乘法单位元:\(\exist e,\forall a\in R,e\times a=a\times e=a\)

\(R=S\)为整数时,\(e=1\)

\(R=R_n(Q)\),n阶实数矩阵集合,\(e=E(n)\)n阶单位矩阵

M6无零因子,:\(\forall a,b\in R,a\times b=0\rightarrow a=0\ or\ b=0\)

则称R为整环

怎么理解"无0因子"?

显然\(R_n(Q)\)不满足M6,因为两个矩阵\(A,B\)乘积是个0矩阵并不能说明\(A\)或者\(B\)矩阵有至少一个是零矩阵

比如

\[ A=\begin{bmatrix} 0\ 0\ 0\\ 0\ 0\ 0\\ 0\ 0\ 1\\ \end{bmatrix}\ \ \ \ \ B=\begin{bmatrix} 0\ 0\ 1\\ 0\ 0\ 0\\ 0\ 0\ 0\\ \end{bmatrix} \] 又如,

\(Z_6=\{0,1,2,3,4,5\}\),

定义\(Z_6\)上的乘法\(\otimes\)\(a\otimes b=(a\times b\ )mod\ 6\)

定义\(Z_6\)上的加法\(\oplus\)\(a\oplus b=(a+b)mod\ 6\)

显然\(Z_6\)满足\(A_1-A_5,M_1-M_5\)

但是\(<Z_6,+,\times>\)不满足无零因子,比如

\(2\otimes 3=(2\times 3)mod \ 6=6mod\ 6=0\)

就是说,0这个元素一定也是在集合R中存在的,并且乘法运算结果为0一定和引入这个元素0有关

\(<R,+,\times>\)为一个整环,如果R再满足

M7乘法逆元:\(\forall a≠0\in R,\exist a^{-1}\in R,a\times a^{-1}=1\)则称\(a^{-1}\)为a的乘法逆元

注意乘法逆元也是\(R\)中的

定义乘法逆元的作用实际上是可以使用除法,除以一个数等于乘以该数的乘法逆元

比如对于\(<Z_7,+,\times>\),由拓展欧几里得定理可知,由于模数为7是一个质数,因此0到6都存在\(Z_7\)上的mod 7意义下的乘法逆元

再比如全体整数就不是任何元素都有乘法逆元,只有1和-1有乘法逆元,任何绝对值大于1的整数其乘法逆元应该是分数,不属于整数.

则称R为域F

有限域GF(p)

符号意义:

\(GF(p)=<Z_p,\oplus,\otimes >\)

\(GF:Galois Field\),伽罗华域

\(p\):表示一个正素数

\(\oplus:\forall a,b\in Z_p,a\oplus b=(a+b)\ mod\ p\)即模p加法

\(\otimes : \forall a,b\in Z_p,a\otimes b=(a\times b)\ mod\ p\)即模屁乘法

为什么一定要求是一个素数?

当N为一个合数的时候\(Z_N=\{1,2,3,...,N-2,N-1\}\)不满足\(M_6\)无零因子,不是整环,因此不是域

为什么不满足\(M_6\)?

由已知,N是合数,则至少存在\(n\in Z_N,1<n<N,n|N\)

如果\(n^2=N\),则有\(n\otimes n=n\times n\ mod\ N=0\)

如果\(n^2!=N\),则\(\exist n'\in (1,N),nn'=N\)那么\(n\otimes n'=nn'\ mod\ N=N\ mod \ N=0\)

故选取p为素数,就是为了保证\(M_6\)无零因子

同时,选取一个素数作为mod值,保证了\(M_7\)乘法逆元:

\(\forall a\in Z_p,a\otimes x=1\),即\(ax\equiv 1(mod\ p)\),显然当p为一个素数时有\(gcd(a,p)=1\)由2拓展欧几里得定理即可求出x的值

因此\(Z_p\)就是一个有限域,用\(GF(p)\)表示

\(Z_p\)上的数论定理

拓展欧几里得定理求乘法逆元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int exgcd(const int &a, const int &b, int &x, int &y) {//拓展欧几里得算法ax+by=1=gcd(a,b)
if (b == 0) {
x = 1;
y = 0;
return a;
}
int x2, y2;
int d = exgcd(b, a % b, x2, y2);
x = y2;
y = x2 - a / b * y2;
return d;
}

int inverse(const int &a, const int &mod) {//求a在模mod下的逆元
int x, y;
exgcd(a, mod, x, y);
return ((x % mod) + mod) % mod;
}

快速幂

1
2
3
4
5
6
7
8
9
10
11
12

int quick_pow(const int &base, const int &index, const int &mod) {//bash^index(% mod)
if (index == 0)
return 1;
else if (index == 1)
return base % mod;
else if (index % 2) {
return quick_pow(base * base % mod, index >> 1, mod) % mod;
} else {
return base * quick_pow(base * base % mod, index >> 1, mod) % mod;
}
}

多项式算术

为什么突然扯到"多项式算数"呢?

前面我们证明了对于整数集\(Z_N=\{0,1,2,3,...,N-1\}\),只有当\(|Z_N|=N\)为素数p时,\(Z_p\)才为一个域

如果想要得到一个元素个数为\(2^n\)的域,显然\(Z_p\)做不到.

为什么要得到一个元素个数为\(2^n\)的域?计算机使用二进制编码,AES加密算法就用到了\(GF(2^8)\)

一个3位二进制数可以表示8中状态,明文和密码就在这八种状态之中

模8乘法 0 1 2 3 4 5 6 7
0 0 0 0 0 0 0 0 0
1 0 1 2 3 4 5 6 7
2 0 2 4 6 0 2 4 6
3 0 3 6 1 4 7 2 5
4 0 4 0 4 0 4 4 3
5 0 5 2 7 4 1 6 3
6 0 6 4 2 4 3 4 2
7 0 7 6 5 3 3 2 1

\(Z_8\)中的数字乘法,其结果中个数字的出现次数显然是不均匀的,比如1出现了4次,2就出现了8次

而一个理想的密码是不能暴露词频信息的,显然用\(Z_8\)上的变换进行加密不理想,于是想一种乘法和除法的构造方法, 使得有8个元素的域其乘法或者加法的结果的频率相等

而使用多项式运算就可以解决这个问题,因此引入了多项式运算

首先要研究的是==如何构造一个有8个元素(\(p^n\))的域==

==将数的性质拓展到多项式式上==

我们使用的一般是代数基本规则的普通多项式运算

然后拓展到系数在\(GF(p)\)中的多项式运算

然后再拓展到系数在\(GF(p)\),模一个\(n\)次多项式\(m(x)\)的多项式运算

最终目标是理解并应用最后一种多项式

普通多项式运算

多项式次数:最高次项的次数

多项式的表示: \[ f(x)=a_nx^n+a_{n-1}x^{n-1}+...+a_1x+a_0=\sum_{i=0}^na_ix^i,a_n≠0 \] 如果\(\forall a_i\in S\)则称\(f(x)\)是系数集\(S\)上的多项式,令\(<A,+,\times >\)表示S上的所有多项式以及加法和乘法二元关系

\(S=Z\)表示整数集的时候,显然A是满足\(A_1-A_5,M_1-M_6\),是个整环,

但是不满足乘法逆元,比如\(x\)的乘法逆元\(x^{-1}\),是一个指数为负的多项式,显然不在\(A\)

\[ f(x)=\sum_{i=0}^na_ix^i\\ g(x)=\sum_{i=0}^mb_ix^i\\ a_nb_m≠0,n\ge m \] 则普通多项式的加法运算 \[ f(x)+g(x)=\sum_{i=0}^m(a_i+b_i)x^i+\sum_{i=m+1}^na_i x^i \] 普通多项式乘法 \[ f(x)g(x)=\sum_{i=0}^{n+m}c_ix^i\\ c_i=a_0b_i+a_1b_{i-1}+...+a_ib_0 \]

系数在\(Z_p\)中的多项式运算

当系数集是全体整数的时候,由于系数不都有乘法逆元,因此无法进行多项式除法

当系数集是一个域的时候,系数都有了乘法逆元,可以对多项式引入除法(带余除法)

可以类比整数的除法,求余数用\(\%\),求商用\(/\)

那么在\(Z_p\)中的多项式,求余式用\(\%\),求商式用\(/\)

比如对于\(Z_2\)上的多项式\(f(x)=x^4+1=(x+1)(x^3+x^2+x+1)\),则\(f(x)/(x+1)=x^3+x^2+x+1\)

设系数域为\(<Z_p,\oplus,\otimes >\)

\(Z_p\)上的两个多项式 \[ f(x)=\sum_{i=0}^na_ix^i\\ g(x)=\sum_{i=0}^mb_ix^i\\ a_n,b_m≠0,n\ge m \]

\(Z_p\)多项式的四则运算

\(Z_p\)上的多项式加法(减法类似)为 \[ f(x)+g(x)=\sum_{i=0}^m(a_i\oplus b_i)x^i+\sum_{i=m+1}^na_i x^i\\ =\sum_{i=0}^m(a_i+ b_i)mod\ p\ \times x^i+\sum_{i=m+1}^na_i x^i \]

\(Z_p\)上的多项式乘法为 \[ f(x)g(x)=\sum_{i=0}^{n+m}c_ix^i\\ c_i=(a_0b_i+a_1b_{i-1}+...+a_ib_0)mod\ p \] \(Z_p\)上的带余式除法 \[ f(x)=q(x)g(x)+r(x) \] 意思是\(f(x)\div g(x)=q(x)...r(x)\)

\(Degree(f)\)表示多项式f的阶,并且\(Degree(f)=n,\\ Degree(g)=m,\\\)则有\(Degree(q)=n-m\\ Degree(r)\le m-1\)

\(Z_p\)多项式的欧几里得定理

定义\(d(x)\)\(a(x),b(x)\)的最大公因式,即\(d(x)\)是能够整除\(a(x),b(x)\)的所有多项式中次数最高的 \[ gcd(a(x),b(x))=gcd[b(x),a(x)\%b(x)] \]

\(Z_p\)上多项式再mod n次素多项式

对于\(Z_p\)上次数高于n-1的多项式\(f(x)\),需要mod一个\(Z_p\)上的n次素多项式\(m(x)\),如此限制\(f(x)\)的次数在\([0,n-1]\)

什么是"素多项式"?\(Z_p\)上的素多项式\(m(x)\)无法被\(Z_p\)上的任意多项式整除

\(m(x)\)的次数为n,则\(Z_p\)上的多项式\(mod\ m(x)\)都会落在次数小于等于n-1的多项式集\(F\)

显然\(F\)中的多项式都可以表示为 \[ \forall f(x)\in F,f(x)=a_0+a_1x+...+a_{n-1}x^{n-1},\forall a_i\in Z_p \] 那么这样的多项式一共有\(p^n\)个(系数的乘法原理),即模\(m(x)\)构成的剩余类

现在类比\(Z_p\),证明\(F\)是一个域

显然加减乘封闭且结合律分配律交换律均满足,\(F\)容易判定为交换环

由于1也是\(F\)中的多项式,因此存在乘法单位元1,

下面证明M6无零因子

由于k阶多项式的k次项系数不为零,假设有两个非零多项式,其最高次项分别为\(a_ix^i,b_jx^j\)

乘积多项式的最高次项为\(a_ib_jx^{i+j}\)如果指数\(i+j\ge n\)则对\(m(x)\)取模,如果系数\(a_ib_j\ge p\)则对\(p\)取模

显然p是一个素数,\(a_ib_j=a_i\times b_j\)是一个合数,合数对素数取模显然不为0

因此任何两个非零多项式乘积一定非零,M6无零因子得证

到此F被证明是整环

无零因子就是\(GF(2^3)\)是域但是\(Z_8\)不是域的本质原因

下面证明任意F中的非0多项式都在F中有乘法逆元

\(f(x)g(x)\equiv 1(mod\ m(x))\)

\(g(x)f(x)+k(x)m(x)=1\)

\(m(x)\)为素因式,有欧几里得定理知上式有解

因此M7乘法逆元得证

因此\(F\)是域

\(F\)\(Z_p\)的不同

我们在证明\(Z_p\)是域的时候发现,对于整数集\(Z_N=\{0,1,2,3,...,N-1\}\),只有当\(|Z_N|=N\)为素数p时,\(Z_p\)才为一个域

而现在\(|F|=p^n\)显然当\(n>1\)时是一个合数,但是\(F\)仍然是一个域

\(F\)记作\(GF(p^n)\)表示伽罗华域

\(F=GF(p^n)\)上的乘法逆元

\[ f(x)g(x)\equiv 1(mod \ m(x))\\ g(x)f(x)+k(x)m(x)=1\\ gcd(f(x),m(x))=1 \]

显然可以将整数上的拓展欧几里得推广到F上

\(GF(2^n)\)上构造结果分布均匀的二元运算

\(GF(2^n)\)上的多项式各项的系数要么是0,要么是1,可以用二进制数表示

比如\(x^3+x^2+1\)就可以表示为1101

加法

由于系数要么是0要么是1,即系数要自动对2取模

那么多项式的加法就是二进制数按位异或

比如\((x^3+x^2+x)+(x^4+x+1)=x^4+x^3+x^2+2x+1=x^4+x^3+x^2+1\)

\(01110\oplus 10011=11101\)

乘法

教材上一本正经地写了一堆用字母表示的多项式,看上去头大.

从一个例子入手可能比较容易理解:

考虑AES加密算法使用到的\(GF(2^8)\),取\(m(x)=x^8+x^4+x^3+x+1\)为模.

\(f(x)=x^6+x^4+x^2+x+1\)

\(g(x)=x^7+x+1\)

\(f(x)\otimes g(x)=f(x)\times g(x)\mod m(x)\)

拆分成项

容易想到的是把\(g(x)\)按幂次拆分成项然后用f(x)与g(x)的各项相乘之后相加,而相加在"加法"中我们已经认识到可以通过两个多项式异或这种简洁的方式实现,因此我们现在把精力放在\(f(x)\)如何和一个\(x^k\)项相乘上 \[ f(x)\times g(x)\mod m(x)\\ =f(x)\times(1+x+x^7)\mod m(x)\\ =f(x)\times 1\mod m(x)+f(x)\times x\mod m(x)+f(x)\times x^7\mod m(x) \] 问题转化为如何求\(f(x)\times x^k\mod m(x)\)

\(f(x)\times x^k\mod m(x)\)

由于 \[ f(x)\times x^k\mod m(x)\\ =\{[(f(x)\times x\mod m(x))\times x\mod m(x)]\times ...\times x\mod m(x)\}\times x\mod m(x) \] 因此问题又可以转换为怎么跨出求\(f(x)\)\(f(x)\times x\mod m(x)\)这第一步

\(f(x)\times x\mod m(x)\)

假设\(f(x)=a_7x^7+a_6x^6+...+a_1x+a_0\)表示\(GF(2^8)\)上的任意多项式

\(x\times f(x)=a_7x^8+a_6x^7+...+a_0x\)

如果用二进制表示,那么 \[ f(x)=&a_7&a_6&a_5&a_4&a_3&a_2&a_1&a_0\\ x\times f(x)=a_7&a_6&a_5&a_4&a_3&a_2&a_1&a_0&0 \] 在还没有取模时,我们可以发现\(f(x)\)\(x\times f(x)\)只需要将\(f(x)\)的二进制表示左移一位,下面考虑如何取模

如果\(a_7=0\)\(x\times f(x)\)顶多是一个7次多项式,如果有\(a_6=0\)则顶多是一个6次多项式,一个七次多项式\(x\times f(x)\)去mod一个8次多项式\(m(x)\)实乃以卵击石,直接被8次多项式劝返

如果\(a_7=1\)\(x\times f(x)\)\(m(x)\)都是8次多项式,算是旗鼓相当,可以一战

此时\(x\times f(x)\)可以分成精锐的头部\(x^8\)与累赘的尾部\(a_6x^7+a_5x^6+...+a_0x\),

这个尾部对\(m(x)\)取模还是被劝返,只留下精锐的头部\(x^8\)独自抗衡\(m(x)\)

那么问题转化为\(x^8\)\(m(x)=x^8+x^4+x^3+x+1\)取模,考虑如何取模?

\(x^8 \mod m(x)\)

\(x^8\)形单影只,只能单挑\(m(x)\)\(x^8\)项,无暇处理\(m(x)\)的一伙子小弟,

于是\(x^8\)利用其系数都在\(GF(2)\)上,无中生有搬来了一伙子小弟: \[ x^8\equiv x^8+2x^7+2x^6+...+2x+2(系数mod 2) \] 此时用\(x^8+2x^7+2x^6+...+2x+2\)\(\mod m(x)\)终于可以大干一场了,还得是门当户对地干,次数相同的项单挑

\(x^8\equiv x^8+2x^7+2x^6+...+2x+2(系数mod 2)\)作为被除数,\(m(x)\)作为除数,余数即为所求结果

战争一开始,商1之后被除数减去除数得到\(2x^7+2x^6+2x^5+x^4+x^3+2x^2+x+1\equiv x^4+x^3+x+1(系数mod2)\)

立刻发现刚才"精锐的头部"那个\(x^8\)在和\(m(x)\)的8次项的决斗中阵亡了,剩下的小弟都是7次方以下的项,无力与\(m(x)\)抗衡,直接作为余数

即刚才的"战争"可以写为: \[ x^8\equiv x^4+x^3+x+1 \mod m(x)\&系数mod2\\ 其中m(x)=x^8+x^4+x^3+x+1 \] 突然发现\(x^8\mod m(x)=m(x)-x^8=x^4+x^3+x+1\)

这是巧合吗?

这是系数mod2的必然结果,并且可以从8次推广到n次:

\(m(x)\)为n次多项式 \[ x^n\mod m(x)=m(x)-x^n(系数mod2) \] \(x^8\)独自面对\(m(x)\),最终壮烈牺牲但是换回\(x^4+x^3+x+1\)颇有"将军百战死,壮士十年归"的感觉

在战争之前我们把"累赘的尾部\(a_6x^7+a_5x^6+...+a_0x\)"留下不参战,原因是他们参战也会被敌人\(m(x)\)直接劝返

现在我们知道了精锐的头部\(x^8\)和累赘的尾部\(a_6x^7+a_5x^6+...+a_0x\)各自参战的结果了,此时可以总结一支部队\(x\times f(x)=x^8+a_6x^7+a_5x^6+...+a_0x\)参战的结果了: \[ x\times f(x)\mod m(x)=[m(x)-x^8]+a_6x^7+a_5x^6+...+a_0x \] 比如当\(f(x)=x^7+x^4+x^2+x+1,m(x)=x^8+x^4+x^3+x+1\)\[ \begin{aligned} &x\times f(x)\mod m(x)\\ &=(x^8+x^5+x^3+x^2+x)\mod (x^8+x^4+x^3+x+1)\\ &=[x^4+x^3+x+1]+[x^5+x^3+x^2+x]\\ &=011011\oplus 101110\\ &=110101 \end{aligned} \] 到此我们知道\(x\times f(x)\mod m(x)\)如何计算了,

那么\(x^2\times f(x)\mod m(x)=x\times (x\times f(x)\mod m(x))\mod m(x)\)

以此类推可以得到\(x^k\times f(x)\mod m(x)\)如何计算了

回到求\(f(x)\times g(x)\mod m(x)\)

不管你\(g(x)\)长什么样,我先预处理出\(f(x)\times x\mod x,f(x)\times x^2\mod m(x),f(x)\times x^k\mod m(x)\)等等情况

如果你\(g(x)=1+x+x^2\)长得很虚,那么 \[ f(x)\times g(x)\mod m(x)\\ =f(x)\times (1+x+x^2)\mod m(x)\\ =(f(x)+x\times f(x)+x^2\times f(x))\mod m(x)\\ =f(x)+x\times f(x)\mod m(x)+x^2\times f(x)\mod m(x) \] 直接利用预处理得出的结果,然后计算加法直接用二进制异或

总结

到此我们构造出了\(GF(2^n)\),即有\(2^n\)个元素的伽罗华域

怎么构造出的?由系数在\(GF(2)\)上的多项式模一个\(2^n\)次的素多项式拓展出的

多项式和这\(2^n\)个数怎么产生联系?每一个多项式都对应一个二进制数

\(2^n\)个数的加减乘除运算怎么定义的?还是通过多项式的运算得到的

下一步可以向AES加密算法进军了

MySQL命令

登录

在linux终端上登录命令为:

1
2
3
>mysql -u<username> -p<password>
>mysql -u<username> -p
>Enter password:<password>

例如:

1
>mysql -uroot -psjh123456

登录成功之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@deutschball-virtual-machine:/home/deutschball/桌
面# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 31
Server version: 8.0.27-0ubuntu0.20.04.1 (Ubuntu)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

或者:

1
2
>mysql -uroot -p
>Enter password:

后面这种方法密码需要另起一行输入,并且输入的时候不会显示在屏幕上,类似于linux终端登录其他用户账号时的情形

退出登录:

1
mysql>exit;

==注意==

1.登录MySQL数据库之后,命令行自动带上mysql>标志,表示此后执行的所有命令均是对MySQL数据库的操作.

2.MySQL命令也是末行模式,并且以分号";"结束命令

查看用户名下所有数据库

1
mysql>show databases;

例如:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| earth |
| empire |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (0.01 sec)

创建数据库

1
>mysql create database <dbname>;

例如:

1
2
mysql> create DATABASE newdb;
Query OK, 1 row affected (0.00 sec)

如果已经存在同名数据库则会报告错误

1
2
mysql> create database newdb;
ERROR 1007 (HY000): Can't create database 'newdb'; database exists

删除数据库

1
mysql>drop database <dbname>;

例如:

1
2
mysql> drop database newdb;
Query OK, 0 rows affected (0.02 sec)

对一个不存在的数据库名使用drop命令会报错:

1
2
mysql> drop database newdb;
ERROR 1008 (HY000): Can't drop database 'newdb'; database doesn't exist

选择数据库

使用show databases命令之后可以看到一系列数据库列表,现在要对其中某个数据库进行操作,应当选中该数据库,类似于cd进入某个子目录进行操作

1
mysql>use <dbname>

例如:

1
2
mysql> use empire;
Database changed

企图使用不存在的数据库将会报错:

1
2
mysql> use noneexist;
ERROR 1049 (42000): Unknown database 'noneexist'

注意此后对数据表的操作需要首先选中数据库

对于单个数据的操作需要首先选中数据表

查看数据表状态

1
SHOW TABLE STATUS LIKE '<tablename>';
status

创建数据表

如果没有事先选中数据库就创建数据表会报错:

1
2
mysql> create TABLE troop;
ERROR 1046 (3D000): No database selected

需要事先使用use命令确定数据库之后才能建立数据表

1
2
3
4
5
6
mysql>CREATE TABLE <tablename>(
column_name1 data_type(size),
column_name2 data_type(size),
...
column_namen data_type(size)
);

其中后面的圆括号中包含若干对数据,分别是列名称和其数据类型(最大长度)

1
2
3
4
5
6
7
8
mysql> use empire;
Database changed
mysql> CREATE TABLE troop(
-> id int,
-> name varchar(255),
-> age int
-> );
Query OK, 0 rows affected (0.02 sec)

在数据类型后面可以声明not null,则新增记录时如果该项的值缺省则报错(不声明not null的缺省值自动设为NULL)

1
2
3
4
5
6
7
8
9
mysql> CREATE TABLE fleets(
-> id INT NOT NULL AUTO_INCREMENT,
-> commander VARCHAR(100) NOT NULL,
-> PRIMARY KEY(id)
-> )ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected, 1 warning (0.02 sec)
mysql> insert into fleets(id)values(2);
ERROR 1364 (HY000): Field 'commander' doesn't have a default value

属性约束

主键约束

外键约束

空约束

去重约束

默认值约束

断言约束

检查约束

删除数据表

1
drop table <tablename>

例如:

1
2
mysql> drop table troop;
Query OK, 0 rows affected (0.01 sec)

插入数据

1
INSERT INTO table_name ( field1, field2,...fieldN )VALUES( value1, value2,...valueN );

前面括号中是列栏目名称,插入不是按照数据表创建时的列顺序插入的,而是value与field一一对应

例如:

1
2
3
4
5
6
7
8
9
10
11
mysql> insert into fleets(commander)values('vader');
Query OK, 1 row affected (0.00 sec)

mysql> select * from fleets;
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
+----+-----------+
1 row in set (0.00 sec)

id已设置自增

查询数据表

1
2
3
4
SELECT column_name,column_name
FROM table_name
[WHERE Clause]
[LIMIT N][ OFFSET M]

select语句列明要查找的列栏目名称

column_name规定保留查询记录的列目,如果只写星号*,则保留完整记录

from语句指明从哪个数据表查询

where语句给出数据筛选条件

limit语句限定查询数据最大条数

offeset设置开始查询的数据偏移量

例如现有数据库如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select *from fleets;
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
| 2 | soldier1 |
| 3 | soldier2 |
| 4 | soldier10 |
| 5 | vader |
| 6 | vader |
| 7 | vader |
| 8 | vader |
| 9 | vader |
| 10 | vader |
| 11 | vader |
| 12 | vader |
+----+-----------+
12 rows in set (0.00 sec)

要查询所有commander=vader的飞船的id记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select id from fleets where commander='vader';
+----+
| id |
+----+
| 1 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
| 11 |
| 12 |
+----+
9 rows in set (0.00 sec)

打印数据表前五个完整记录:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select * from fleets limit 5;
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
| 2 | soldier1 |
| 3 | soldier2 |
| 4 | soldier10 |
| 5 | vader |
+----+-----------+
5 rows in set (0.00 sec)

WHERE

类似于if语句,用于限定查询,删除等操作的范围.

可以理解为:在满足某某条件的记录上进行某种操作

比如:

1
mysql> select id from fleets where commander='vader';

这句就可以翻译为:

从fleets数据表中查询所有commander列的值为'vader'的记录,返回满足条件的记录的id

where条件为bool表达式

注意判断是否相等只需要使用单等号=,不需要使用双等号==

1
mysql> select * from fleets where id%2=0;

这句可以翻译为:

从fleets数据表中查询所有id为偶数的记录

结果:

1
2
3
4
5
6
7
8
9
10
11
+----+-----------+
| id | commander |
+----+-----------+
| 2 | soldier1 |
| 4 | soldier10 |
| 6 | vader |
| 8 | vader |
| 10 | vader |
| 12 | vader |
+----+-----------+
6 rows in set (0.00 sec)

where中使用and指定多个条件

1
2
3
4
5
6
7
8
9
mysql> select *from fleets where id%2=0 and id%3=0;
+----+-----------+
| id | commander |
+----+-----------+
| 6 | vader |
| 12 | vader |
+----+-----------+
2 rows in set (0.00 sec)

从fleets数据表中查询所有id为2和3的公倍数的记录

where使用binary开启大小写敏感

现有数据表如下(注意第13条记录Vader有大写),要查询所有commander='vader'的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select *from fleets;
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
| 2 | soldier1 |
| 3 | soldier2 |
| 4 | soldier10 |
| 5 | vader |
| 6 | vader |
| 7 | vader |
| 8 | vader |
| 9 | vader |
| 10 | vader |
| 11 | vader |
| 12 | vader |
| 13 | Vader |
+----+-----------+
13 rows in set (0.00 sec)

使用binary关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select *from fleets where binary commander='vader';
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
| 5 | vader |
| 6 | vader |
| 7 | vader |
| 8 | vader |
| 9 | vader |
| 10 | vader |
| 11 | vader |
| 12 | vader |
+----+-----------+
9 rows in set, 1 warning (0.00 sec)

如果不用binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select *from fleets where commander='vader';
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
| 5 | vader |
| 6 | vader |
| 7 | vader |
| 8 | vader |
| 9 | vader |
| 10 | vader |
| 11 | vader |
| 12 | vader |
| 13 | Vader |
+----+-----------+
10 rows in set (0.00 sec)

数据表更新记录

1
2
UPDATE table_name SET field1=new-value1, field2=new-value2,...
[WHERE Clause]

将fleets数据表中id=5的记录的指挥官重新指派为anakin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> update fleets set commander='anakin' where id=5;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select *from fleets;
+----+-----------+
| id | commander |
+----+-----------+
| 1 | vader |
| 2 | soldier1 |
| 3 | soldier2 |
| 4 | soldier10 |
| 5 | anakin |
| 6 | vader |
| 7 | vader |
| 8 | vader |
| 9 | vader |
| 10 | vader |
| 11 | vader |
| 12 | vader |
| 13 | Vader |
+----+-----------+
13 rows in set (0.00 sec)

数据表删除记录

1
DELETE FROM table_name [WHERE Clause]

从数据表fleets中删除编号id=1的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> delete from fleets where id=1;
Query OK, 1 row affected (0.01 sec)

mysql> select * from fleets;
+----+-----------+
| id | commander |
+----+-----------+
| 2 | soldier1 |
| 3 | soldier2 |
| 4 | soldier10 |
| 5 | anakin |
| 6 | vader |
| 7 | vader |
| 8 | vader |
| 9 | vader |
| 10 | vader |
| 11 | vader |
| 12 | vader |
+----+-----------+
11 rows in set (0.00 sec)

删除后打印数据表发现第一条记录的编号为2,表明id虽然自动增加,但是删除中间的某些记录后,后面的记录的id不会自动减小

如果不使用where子句则删除指定数据表中的所有记录

1
2
3
4
5
6
mysql> delete from fleets;
Query OK, 11 rows affected (0.00 sec)

mysql> select * from fleets;
Empty set (0.00 sec)

数据表模糊匹配操作

1
2
3
SELECT field1, field2,...fieldN 
FROM table_name
WHERE field1 LIKE condition1 [AND [OR]] filed2 = 'somevalue'

关键由LIKE子句实现,首先容易观察得到的是LIKE子句完全包含等号的作用

可以理解为:

从某数据表查询某键值==像==某个样子的记录

比如从动物园数据表中查询==狗样==的记录和从动物园查询所有==狗==的记录,前者为LIKE语句,后者为等号

比如现有数据表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select * from fleets;
+----+------------+
| id | commander |
+----+------------+
| 14 | soldier1 |
| 15 | soldier2 |
| 16 | soldier3 |
| 17 | soldier20 |
| 18 | soldier200 |
| 19 | general |
| 20 | admiral |
| 21 | major |
+----+------------+
8 rows in set (0.00 sec)

现在要查询所有commander为soldier==#==(这里#表示任意字符或者字符串)的记录,显然用等号的话只能用where和or进行枚举,

1
mysql> select * from fleets where commander='soldier1' or commander='soldier2' or commander='soldier3'...

可以翻译为:

从数据表fleets中查询所有指挥官为soldier1或者soldier2或者soldier3...的记录

但是用==like配合占位符%==进行模糊搜索

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select * from fleets where commander like 'soldier%';
+----+------------+
| id | commander |
+----+------------+
| 14 | soldier1 |
| 15 | soldier2 |
| 16 | soldier3 |
| 17 | soldier20 |
| 18 | soldier200 |
+----+------------+
5 rows in set (0.01 sec)

可以翻译为:

从数据表fleets中查询所有指挥官为soldier后面加上一些东西(管他什么东西,甚至是没有东西)的记录

数据表查找合并

1
2
3
4
5
6
7
SELECT expression1, expression2, ... expression_n
FROM tables
[WHERE conditions]
UNION [ALL | DISTINCT]
SELECT expression1, expression2, ... expression_n
FROM tables
[WHERE conditions];

tables为数据表名

expression为要检索的列

distinct为去重合并,all为不去重合并

例如在empire数据库下有两个数据表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from officers;
+----+----------------+------------+
| id | name | title |
+----+----------------+------------+
| 1 | Darth Vader | executor |
| 2 | Darth Sidious | emperor |
| 3 | Wilhuff Tarkin | Grand Moff |
+----+----------------+------------+
3 rows in set (0.00 sec)

mysql> select *from commanders;
+----+---------------+---------+
| id | name | title |
+----+---------------+---------+
| 1 | Rex | captain |
| 2 | Darth Vader | general |
| 3 | Darth Sidious | marshal |
+----+---------------+---------+
3 rows in set (0.00 sec)

1.现在统计所有政府官员和军队指挥官一共有多少人(考虑有些人可以集军政大权于一身,需要去重)

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> select name from officers
-> union
-> select name from commanders;
+----------------+
| name |
+----------------+
| Darth Vader |
| Darth Sidious |
| Wilhuff Tarkin |
| Rex |
+----------------+
4 rows in set (0.01 sec)

2.统计名叫Darth Vader的是否身兼数职

1
2
3
4
5
6
7
8
9
10
11
mysql> select *from officers where name='Darth Vader'
-> union all
-> select *from commanders where name='Darth Vader';
+----+-------------+----------+
| id | name | title |
+----+-------------+----------+
| 1 | Darth Vader | executor |
| 2 | Darth Vader | general |
+----+-------------+----------+
2 rows in set (0.00 sec)

3.统计==肉眼可见的西斯==担任的职务:

肉眼可见的西斯即Darth开头

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> select * from officers where name like 'Darth%' 
-> union all
-> select *from commanders where name like 'Darth%';
+----+---------------+----------+
| id | name | title |
+----+---------------+----------+
| 1 | Darth Vader | executor |
| 2 | Darth Sidious | emperor |
| 2 | Darth Vader | general |
| 3 | Darth Sidious | marshal |
+----+---------------+----------+
4 rows in set (0.00 sec)

排序

1
2
SELECT field1, field2,...fieldN FROM table_name1, table_name2...
ORDER BY field1 [ASC [DESC][默认 ASC]], [field2...] [ASC [DESC][默认 ASC]]

order by语句中从前到后为关键字优先级,首先按照field1关键字的规则进行排序,然后field1关键字相同项再按照field2关键字进行排序,以此类推

ASC(ascend)升序,默认模式

DESC(descent)降序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select *from commanders order by id ASC;
+----+---------------+---------+
| id | name | title |
+----+---------------+---------+
| 1 | Rex | captain |
| 2 | Darth Vader | general |
| 3 | Darth Sidious | marshal |
+----+---------------+---------+
3 rows in set (0.00 sec)

mysql> select *from commanders order by id DESC;
+----+---------------+---------+
| id | name | title |
+----+---------------+---------+
| 3 | Darth Sidious | marshal |
| 2 | Darth Vader | general |
| 1 | Rex | captain |
+----+---------------+---------+
3 rows in set (0.00 sec)

分组

1
2
3
4
SELECT column_name, function(column_name)
FROM table_name
WHERE column_name operator value
GROUP BY column_name;

从某个表中按照某些规则选取某些列,并且按照某列进行同名分组

现有数据库如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> select * from 
-> popularity;
+----+------+--------+
| id | name | gender |
+----+------+--------+
| 1 | Tom | male |
| 2 | Tom | male |
| 3 | Tom | male |
| 4 | Jack | male |
| 5 | Jon | famal |
| 6 | Mike | male |
+----+------+--------+
6 rows in set (0.00 sec)

要调查人口统计表中人重名的情况

1
2
3
4
5
6
7
8
9
10
11
mysql> select name,count(*) from popularity group by name;
+------+----------+
| name | count(*) |
+------+----------+
| Tom | 3 |
| Jack | 1 |
| Jon | 1 |
| Mike | 1 |
+------+----------+
4 rows in set (0.01 sec)

WITH ROLLUP

现有彩票获奖名单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select * from Winners;
+----+------+-------+
| id | name | prize |
+----+------+-------+
| 1 | Mike | 20 |
| 2 | Mike | 30 |
| 3 | Mike | 10 |
| 4 | Mike | 15 |
| 5 | Jack | 15 |
| 6 | Jack | 18 |
| 7 | John | 18 |
| 8 | Jack | 19 |
| 9 | Mike | 5 |
+----+------+-------+
9 rows in set (0.00 sec)

1.查看每个人一共获奖多少次

1
2
3
4
5
6
7
8
9
mysql> select name,count(*) from Winners group by name;
+------+----------+
| name | count(*) |
+------+----------+
| Mike | 5 |
| Jack | 3 |
| John | 1 |
+------+----------+
3 rows in set (0.00 sec)

2.查看每个人一共获奖多少钱

1
2
3
4
5
6
7
8
9
10
mysql> select name,SUM(prize) as tot_prize from Winners group by name with rollup;
+------+-----------+
| name | tot_prize |
+------+-----------+
| Jack | 52 |
| John | 18 |
| Mike | 80 |
| NULL | 150 |
+------+-----------+
4 rows in set (0.00 sec)

其中SUM()为累加函数,类似的有AVG()平均数函数,COUNT()等

NULL为总计,即三个获奖者奖金总和

如果想让结果显示"prize_sum"字样而不是NULL则可以用select coalesce(...)函数

1
select coalesce(a,b,c);

如果a=NULL则选择b,如果a,b=NULL则选择c,全空则为NULL

1
2
3
4
5
6
7
8
9
10
mysql> select coalesce(name,'sum_prize'),SUM(prize) as tot_prize from Winners group by name with rollup;
+----------------------------+-----------+
| coalesce(name,'sum_prize') | tot_prize |
+----------------------------+-----------+
| Jack | 52 |
| John | 18 |
| Mike | 80 |
| sum_prize | 150 |
+----------------------------+-----------+
4 rows in set (0.00 sec)

连接

INNER JOIN

img

==等值连接==,获取两张表中匹配的记录

现有帝国政府官员和军队指挥官的数据表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> select * from officers;
+----+---------+-----------+
| id | name | title |
+----+---------+-----------+
| 1 | Mike | Mayor |
| 2 | Jack | executor |
| 3 | Jackson | candidate |
| 4 | John | emperor |
| 5 | Tom | minister |
+----+---------+-----------+
5 rows in set (0.00 sec)

mysql> select * from commanders;
+----+-------+---------------+
| id | name | military_rank |
+----+-------+---------------+
| 1 | Vader | general |
| 2 | Rex | captain |
| 3 | John | marshal |
+----+-------+---------------+
3 rows in set (0.00 sec)

现在John皇帝不希望军队和政治耦合,希望调查有没有在军政上同时身居要职的大官,

即统计其中身兼数职(比如John既是帝国皇帝又是军队元帅)的人

1
2
3
4
5
6
7
mysql> select a.name,a.title,b.military_rank from officers a inner join commanders b on a.name=b.name;
+------+---------+---------------+
| name | title | military_rank |
+------+---------+---------------+
| John | emperor | marshal |
+------+---------+---------------+
1 row in set (0.00 sec)

其中

1
mysql> select a.name,a.title,b.military_rank from officers a inner join commanders b on a.name=b.name;

可以翻译为:

保留a表的name和title列,保留b表的military_rank列,

a表即officers表,b表即commanders表,

两表根据name列等值连接

调查完后John皇帝很开心

LEFT JOIN

左合并会保留左侧表的全部数据,不管右侧表有无匹配数据

img
1
2
3
4
5
6
7
8
9
10
11
12
mysql> select a.name,a.title,b.military_rank from officers a left join commanders b on a.name=b.name;
+---------+-----------+---------------+
| name | title | military_rank |
+---------+-----------+---------------+
| Mike | Mayor | NULL |
| Jack | executor | NULL |
| Jackson | candidate | NULL |
| John | emperor | marshel |
| Tom | minister | NULL |
+---------+-----------+---------------+
5 rows in set (0.00 sec)

可以翻译为:

政府官员先都列在表里,然后军队指挥官如果有人也是政府官员则把其军衔也写在表里,否则写NULL

RIGHT JOIN

类比左合并,保留右侧表的全部数据,不管左侧表有无匹配数据

NULL值处理

null值与任何值的任何比较都是null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> select * from officers;
+----+---------+-----------+
| id | name | title |
+----+---------+-----------+
| 1 | Mike | Mayor |
| 2 | Jack | executor |
| 3 | Jackson | candidate |
| 4 | John | emperor |
| 5 | Tom | minister |
+----+---------+-----------+
5 rows in set (0.00 sec)

mysql> select * from officers where null=null or null >null or null<null;
Empty set (0.00 sec)

判断NULL

IS NULL,IS NOT NULL,<=>三种方法

现有数据表

1
2
3
4
5
6
7
8
9
10
mysql> select * from officers;
+----+------+---------+
| id | name | post |
+----+------+---------+
| 1 | John | Mayor |
| 2 | Mike | NULL |
| 3 | Jack | NULL |
| 4 | Tom | emperor |
+----+------+---------+
4 rows in set (0.00 sec)
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
mysql> select * from officers where post is not null;
+----+------+---------+
| id | name | post |
+----+------+---------+
| 1 | John | Mayor |
| 4 | Tom | emperor |
+----+------+---------+
2 rows in set (0.00 sec)

mysql> select * from officers where post is null;
+----+------+------+
| id | name | post |
+----+------+------+
| 2 | Mike | NULL |
| 3 | Jack | NULL |
+----+------+------+
2 rows in set (0.00 sec)

mysql> select * from officers where post <=> null;
+----+------+------+
| id | name | post |
+----+------+------+
| 2 | Mike | NULL |
| 3 | Jack | NULL |
+----+------+------+
2 rows in set (0.00 sec)

替换NULL

1
ifnull(a,b)

如果a为NULL则返回b的值

1
2
3
4
5
6
7
8
mysql> select * from officers where ifnull(post,'')='';
+----+------+------+
| id | name | post |
+----+------+------+
| 2 | Mike | NULL |
| 3 | Jack | NULL |
+----+------+------+
2 rows in set (0.00 sec)

一般用于int值替换成0参与计算

正则

在where子句中使用正则表达式,类似于等号和LIKE语句:

1
where 键值 REGEXP <pattern>

现有数据表如下:

1
2
3
4
5
6
7
8
9
10
11
mysql> select * from officers;
+----+---------------+----------+
| id | name | post |
+----+---------------+----------+
| 1 | John | Mayor |
| 2 | Mike | NULL |
| 3 | Jack | NULL |
| 5 | Darth Vader | executor |
| 6 | Darth Sidious | emperor |
+----+---------------+----------+
5 rows in set (0.00 sec)

要查询政府官员中肉眼可见的西斯

1
2
3
4
5
6
7
8
mysql> select * from officers where name regexp '^Darth';
+----+---------------+----------+
| id | name | post |
+----+---------------+----------+
| 5 | Darth Vader | executor |
| 6 | Darth Sidious | emperor |
+----+---------------+----------+
2 rows in set (0.00 sec)

MySQL正则表达式规则与javaScript等脚本语言中的正则表达式规则相同

事务

意义

事务这种东西的存在我的理解是:

使用命令行操作数据库,很难保证输入没有拼写错误或者语法错误.

比如某个人口统计数据库里有一张基本统计表,一张学历统计表,一张收入统计表.

现在张三寿终正寝over了,需要在这三张表中都删除张三的记录.

如果没有事务,我们需要分别在三张表上各执行一次删除操作,如果在第二张表上删除时写成了张四那么张四就可能无缘无故地在某表上去世了,但是该走的张三没走.

更进一步,如果一口气要删除近一个月的死亡人口,需要输入多个名字,很难保证在三张表上准确地删除这些人名

这时引入事务,即规定一个事务开始,然后写入想要执行的命令,然后规定事务结束,此时确认事务内输入无误后再命令事务执行,可以有效减少错误.

如果肉眼没有检查出事务输入的错误,事务自动报错,并且从事务开始到错误的命令都会当作没有执行过

  • 事务的好处用科学的语言表达为:
  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性:数据库允许多个==并发==事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

控制语句

  • BEGIN 或 START TRANSACTION 显式地开启一个事务;

  • COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;

  • ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;

    写着写着发现前面已经有语法错误或者达不到目的甚至偏离目的的语句时,使用回滚,如果运气好设置了savepoint就不至于回滚到从头开始

  • SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个保存点,一个事务中可以有多个 SAVEPOINT;

    可以理解为游戏的复活点(undertale里复活点好像就叫savepoint),从该复活点之后死亡可以回到该复活点,在MySQL中从某个savepoint之后出现错误可以回到该savepoint避免错误

  • RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;

  • ROLLBACK TO identifier 把事务回滚到标记点;

  • SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。

例如

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
mysql> select * from officers;
+----+---------------+----------+
| id | name | post |
+----+---------------+----------+
| 1 | John | Mayor |
| 3 | Jack | NULL |
| 5 | Darth Vader | executor |
| 6 | Darth Sidious | emperor |
+----+---------------+----------+
4 rows in set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> delete from officers where name='Jack';
Query OK, 1 row affected (0.00 sec)

mysql> savepoint point1;
Query OK, 0 rows affected (0.00 sec)

mysql> delete from officers where name='John';
Query OK, 1 row affected (0.00 sec)

mysql> rollback to point1;
Query OK, 0 rows affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from officers;
+----+---------------+----------+
| id | name | post |
+----+---------------+----------+
| 1 | John | Mayor |
| 5 | Darth Vader | executor |
| 6 | Darth Sidious | emperor |
+----+---------------+----------+
3 rows in set (0.00 sec)

1
2
3
4
5
6
第12行事务开始
第15行删除,删除名叫Jack的记录
第18行存档,存档名叫point1
第21行删除,删除名叫John的记录
第24行回滚,回滚到point1,刚才从point1到第24行之间输入的东西都不算数
第27行提交事务

实际执行了只有

1
2
mysql> delete from officers where name='Jack';
Query OK, 1 row affected (0.00 sec)

ALTER修改表字段

在学到这里之前,建表都是使用create命令,规定好列的各种属性以及有多少列,此后就只能对记录进行增删改查的操作了,但是如果想增删字段或者设置字段属性,除了drop了这个表然后新建表,没有其他方法.

现在有了ALTER方法修改字段

修改表名

1
ALTER TABLE <old tablename> RENAME TO <new tablename>;
1
2
mysql> alter table officers rename to government_officers;
Query OK, 0 rows affected (0.01 sec)

查看表字段SHOW COLUMNS

数据表字段及属性由show columns方法可以查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> use empire;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show columns from officers;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| post | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
3 rows in set (0.02 sec)

即officers数据表有三个字段,分别为id,name,post,其类型,是否可以为空,是否为主键,默认值,额外属性业已给出

新增ADD

现在希望增加一个军衔字段.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> alter table officers add military_rank varchar(10) not null default 'soldier';
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show columns from officers;
+---------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| post | varchar(20) | YES | | NULL | |
| military_rank | varchar(10) | NO | | soldier | |
+---------------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

其中

1
mysql> alter table officers add military_rank varchar(10) not null default 'soldier';

可以翻译为:

改变officers这张表的字段值,新增一个military_rank字段,其类型为varchar,最长10个字符,不能为空,缺省值为'soldier';

现在希望在name后面,post前面新增一个字段salary,表示官员薪水,可以使用AFTER命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> alter table officers add salary int(10) not null default 3000 after name;
Query OK, 0 rows affected, 1 warning (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 1

mysql> show columns from officers;
+---------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| salary | int | NO | | 3000 | |
| post | varchar(20) | YES | | NULL | |
| military_rank | varchar(10) | NO | | soldier | |
+---------------+-------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

删除字段DROP

1
ALTER TABLE <tablename> DROP <key>;

现在希望删除salary字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> alter table officers drop salary;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show columns from officers;
+---------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| post | varchar(20) | YES | | NULL | |
| military_rank | varchar(10) | NO | | soldier | |
+---------------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

只修改字段属性MODIFY

1
2
3
mysql> alter table officers modify post varchar(10);
Query OK, 2 rows affected (0.03 sec)
Records: 2 Duplicates: 0 Warnings: 0

修改字段名及属性CHANGE

1
ALTER TABLE <tablename> MODIFY <old key> <new key> <type> ...

现在希望修改post字段为position

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> alter table officers change post position varchar(10);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show columns from officers;
+---------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| position | varchar(10) | YES | | NULL | |
| military_rank | varchar(10) | NO | | soldier | |
+---------------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

需要注意的是,即使只想修改字段名,不改变字段类型等属性,也需要在change语句的新字段后面重写一遍属性

change子句只写旧新字段名会报错:

1
2
mysql> alter table officers change post position;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1

修改字段默认值

1
ALTER TABLE <tablename> ALTER <key> SET <属性> <新属性值>

比如希望修改官员的默认职位为'大臣'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> alter table officers alter position set default 'minister';
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show columns from officers;
+---------------+-------------+------+-----+----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+----------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| position | varchar(10) | YES | | minister | |
| military_rank | varchar(10) | NO | | soldier | |
+---------------+-------------+------+-----+----------+----------------+
4 rows in set (0.00 sec)

修改约束

1
2
alter table UserId
  add constraint PK_UserId primary key (UserId)

索引

普通索引没有任何限制,唯一索引要求==建立索引的列==没有重复数据

显示索引

1
SHOW INDEX FROM <tablename>;
index

普通索引

建立普通索引的方法:

1
2
3
4
5
6
7
8
9
1.CREATE INDEX <indexname> ON <tablename>(columnname);
2.ALTER TABLE <tablename> ADD INDEX <indexname>(columnname);
3.建表时指定
mysql> create table testIndex(
-> id int not null,
-> name varchar(20)not null,
-> index myindex(name)
-> );
Query OK, 0 rows affected (0.02 sec)

删除索引

1
DROP INDEX <indexname> ON <tablename>
1
2
3
mysql> drop index myindex on testIndex;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

唯一索引

建立唯一索引的列不允许有重复数据

建立索引的方法

1
2
3
4
5
6
7
8
9
1.CREATE UNIQUE INDEX <indexname> ON <tablename>(columnname);
2.ALTER TABLEL <tablename> ADD UNIQUE <indexname>(columnname);
3.建表时指定
mysql> create table mytable(
-> id int(10)not null,
-> name varchar(20)not null,
-> unique myindex(name)
-> );
Query OK, 0 rows affected, 1 warning (0.01 sec)
1
2
3
4
5
6
7
8
9
10
mysql> create unique index myindex on mytable(id);
ERROR 1146 (42S02): Table 'empire.mytable' doesn't exist
mysql> create unique index myindex on testIndex(id);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> alter table testIndex add unique myindex2(name);
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0

注意指定length长度

表克隆

"假"克隆

1
SHOW CREATE TABLE <tablename>;

该条指令的作用是显示创建当前表时的创建语句.那么使用相同的语句,稍微改动一下表名称就可以"克隆"一个新表了

但是命令行对与复制粘贴不友好,只能是对着给出的建表语句敲代码,显然这种方法不可取

"真"克隆

1
2
3
4
CREATE TABLE <target_table_name> LIKE <source_table_name>;//克隆表结构
INSERT INTO <target_table_name> SELECT * FROM <source_table_name>;//克隆表数据
CREATE TABLE <target_table_name> SELECT * FROM <source_table_name>;//一步到位
CREATE TABLE <target_table_name> SELECT * FROM <source_table_name> where 1=2;//克隆表结构

现有数据表officers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> show columns from officers;
+---------------+-------------+------+-----+----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+----------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| position | varchar(10) | YES | MUL | minister | |
| military_rank | varchar(10) | NO | MUL | soldier | |
+---------------+-------------+------+-----+----------+----------------+
4 rows in set (0.00 sec)

mysql> select * from officers;
+----+---------------+----------+---------------+
| id | name | position | military_rank |
+----+---------------+----------+---------------+
| 5 | Darth Vader | executor | soldier |
| 6 | Darth Sidious | emperor | soldier |
+----+---------------+----------+---------------+
2 rows in set (0.00 sec)


希望克隆一张新表newOfficers,其结构和内容与officers完全相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> create table newOfficers like officers;
Query OK, 0 rows affected (0.02 sec)

mysql> show columns from newOfficers;
+---------------+-------------+------+-----+----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+-------------+------+-----+----------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | NO | | NULL | |
| position | varchar(10) | YES | MUL | minister | |
| military_rank | varchar(10) | NO | MUL | soldier | |
+---------------+-------------+------+-----+----------+----------------+
4 rows in set (0.00 sec)

mysql> select * from newOfficers;
Empty set (0.00 sec)

通过show columnsselect *两个命令可以观察得到,新表目前只是克隆了结构,但是内容没有克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> insert into newOfficers select * from officers;
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0

mysql> select * from officers;
+----+---------------+----------+---------------+
| id | name | position | military_rank |
+----+---------------+----------+---------------+
| 5 | Darth Vader | executor | soldier |
| 6 | Darth Sidious | emperor | soldier |
+----+---------------+----------+---------------+
2 rows in set (0.00 sec)

执行了mysql> insert into newOfficers select * from officers;之后才完成了内容的复制

一步到位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> create table newtable select * from officers;
Query OK, 2 rows affected (0.02 sec)
Records: 2 Duplicates: 0 Warnings: 0

mysql> select *from newtable;
+----+---------------+----------+---------------+
| id | name | position | military_rank |
+----+---------------+----------+---------------+
| 5 | Darth Vader | executor | soldier |
| 6 | Darth Sidious | emperor | soldier |
+----+---------------+----------+---------------+
2 rows in set (0.00 sec)