首页 > 其他分享 >CSAPP Lab5 Cache Lab

CSAPP Lab5 Cache Lab

时间:2024-04-24 13:33:23浏览次数:12  
标签:CSAPP int Cache 矩阵 Lab times ii jj 高速缓存

到实验 5 啦!

这次的实验是有关高速缓存的。

让我们先来复习一下高速缓存的基础知识吧!

复习

高速缓存的结构

./20230905-csapp-cachelab/image-20230905204348904

在一个存储器地址有 \(m\) 位的系统上,一共有 \(M = 2^m\) 个地址。假设高速缓存被组织成一个有 \(S = 2^s\) 个高速缓存组的数组,其中每个组包括 \(E\) 个高速缓存行,每行存储一个大小为 \(B = 2^b\) 的数据块。每行中还有一个有效位指明该行中是否有存放有效数据,\(t\) 个标记位可以用来来配合组号唯一确定存放在这个行中的块。

\(C = S\times E \times B\) 就是高速缓存的大小。

地址的结构

./20230905-csapp-cachelab/image-20230905205237070

那么,如何确定一个地址应该放在什么位置呢?

高速缓存的参数 \(S\) 和 \(B\) 将每个地址分成了三段。每个地址最低的 \(b\) 位是块偏移,高速缓存中每个块都是连续的 \(B\) 个地址上的数据,块偏移就是用来确定这个地址会位于所在块的那个位置。

第 \(b\) 到 \(b + s - 1\) 位是组索引,用来确定这个地址应该存在高速缓存块中哪一个组里。

而当把一个数据块放进高速缓存中时,剩下的最高 \(t\) 位就是存在高速缓存行中的标记位。也就是说,一定会有 \(t + s + b = m\)。

高速缓存的替换策略

有了前面的基本知识,高速缓存的组选择是很容易理解的。如果我们确定了需要访问的地址的组编号,可以很快定位到这个组中。

如果这个组中有我们需要的块,那么可以直接返回命中结果。

如果组中没有我们需要的块,那么如果组中仍有空位,那么可以从内存中取出这个块存放在组里。但是,如果缓存组已经满了,那么就需要选择一行替换掉。那么该选择哪一行呢?

书上提供了两种常用的策略:

  • 最不常使用(LFU):替换在过去某个窗口时间内引用次数最少的那一行。
  • 最近最少使用(LRU):替换最后一次访问时间最久远的那一行。

实验中让我们使用的策略是 LRU。

Part A: Writing a Cache Simulator

A 的任务很简单,只需在 csim.c 中模拟高速缓存的实现,可以根据提供的内存访问记录的文件来给出命中、不命中和驱逐的统计。这个程序还需要实现对于命令行参数的加载。

所谓的内存访问记录,是使用命令 valgrind 来得到的,其输出是这样的结构:

[space]operation address,size

其中,operationI, M, L, S 中的一种,I 代表指令加载,L 代表数据加载,S 代表数据存储,M 代表数据修改。而 address 是操作的内存地址,size 是操作的数据大小。

分析

因为实验要求中,我们不需要关心指令加载的操作。同时,实验也假设了内存访问被对齐得很好,不会出现一次访问的范围超出了一个块的情况,因此也不需要关心 size

L, S 操作都是对内存的简单访问,M 就相当于是先 LS,而在本实验中,LS 是不做区别的,因此不管是什么操作,都可以看做一次或两次简单的数据访问,在我们的程序中,使用 access(addr) 函数来解决就可以了。

数据表示

typedef struct {            // 结构体:高速缓存行
    bool valid;             // 有效位
    unsigned tag;           // 标记位
    unsigned lastVis;       // 上一次访问的时间
} CacheLine;
typedef struct {            // 结构体:高速缓存组
    CacheLine *lines;       // 组中的所有行
    unsigned count;         // 组中存放有效数据的行数
} CacheSet;
typedef struct {            // 结构体:高速缓存
    CacheSet *sets;         // 高速缓存的所有组
    unsigned S, s;          // 组数
    unsigned E;             // 行数
    unsigned B, b;          // 块大小
    unsigned time;          // 高速缓存的“时间”
} Cache;

我们使用三个结构体分别表示一个高速缓存行、一个高速缓存组和一个高速缓存。

高速缓存的时间表示

你很有可能已经注意到了,在 Cahce 结构体中已经存在了一个 times 成员变量。这就是我们的高速缓存时间。

高速缓存时间是为了标识每个高速缓存行最后一次访问的时间先后,以此来确定要驱逐的行。但是高速缓存时间是怎么设定的呢?

我们注意到,只有在访问数据的时候,会引起每个行的「最后访问时间」发生变化,其余的时间对于替换策略没有什么意义。因此,我们可以在每次访问数据的时候,将高速缓存的 time 加一,以此来决定高速缓存的时间。

Cache ca;
void initCache(int s, int E, int b) {
    ca.s = s, ca.E = E, ca.b = b;
    ca.S = 1 << s, ca.B = 1 << b;
    ca.sets = (CacheSet *)malloc(sizeof(CacheSet) * ca.S);
    for (unsigned i = 0; i < ca.S; ++i) {
        ca.sets[i].count = 0;
        ca.sets[i].lines = (CacheLine *)malloc(sizeof(CacheLine) * E);
        for (unsigned j = 0; j < E; ++j)
            ca.sets[i].lines[j].valid = 0;
    }
}

void access(unsigned addr) {
    ++ca.time;
    // ...
}

一些封装了的操作

在一个组中根据 tag 查找行

简单地扫描组中的所有行即可,因为查找只会发生在数据访问中,所以可以顺便设置一下时间。

CacheLine *findLine(CacheSet s, unsigned tag) {
    for (unsigned i = 0; i < s.count; ++i)
        if (s.lines[i].valid && s.lines[i].tag == tag) {
            s.lines[i].lastVis = ca.time;
            return &s.lines[i];
        }
    return NULL;
}

加载一个新的行

这里写了两个函数,第一个负责在一行设置有效位、标记位和时间,另一个负责在一组中新开一个行(在组没有满的时候)。

void setLine(CacheLine *l, unsigned tag) {
    l->valid = 1;
    l->tag = tag;
    l->lastVis = ca.time;
}
void newBlock(CacheSet *s, unsigned tag) {
    setLine(&s->lines[s->count], tag);
    ++s->count;
}

执行 LRU 策略

就是在一组已满的时候,执行 LRU 策略,找出最后访问时间最早的行。直接模拟即可。

CacheLine *findLRU(CacheSet s) {
    unsigned id = 0;
    for (unsigned i = 1; i < ca.E; ++i)
        if (s.lines[i].lastVis < s.lines[id].lastVis)
            id = i;
    return &s.lines[id];
}

访问数据

大致思路就是先锁定组号,计算标记位。根据我们前面复习的,一个地址前 \(t\) 位是标记位,中间的 s 位是组号,最后的 b 位是偏移,因此标记位可以通过 addr >> (b + s) 计算,组号需要先右移 b 位以后对低 s 位取 and

然后取出这个组,在组中查找标记是否存在。如果不命中,再判断组是否已满。如果已满就驱逐一行,否则直接接着放置即可。

在每一种情况确认以后,记得修改命中、不命中、驱逐计数器,以及在 verbose 模式下需要输出。

int hitc, missc, evic;
void access(unsigned addr) {
    ++ca.time;
    unsigned tag = addr >> (ca.b + ca.s), set = (addr >> ca.b) & (ca.S - 1);
    CacheLine *l = findLine(ca.sets[set], tag);
    if (l) {
        if (verbose) printf(" hit");
        ++hitc;
        return;
    }
    if (verbose) printf(" miss");
    ++missc;
    CacheSet *s = &ca.sets[set];
    if (s->count == ca.E) {
        if (verbose) printf(" eviction");
        ++evic;
        CacheLine *ev = findLRU(*s);
        setLine(ev, tag);

    } else newBlock(s, tag);
}

读取 trace 文件

如前面所述,LS 操作都可以看做一次 accessM 操作可以看作两次 access。根据情况调用 access 函数即可。

注意 verbose 模式下需要输出记录。

void readTrace(FILE *fp) {
    char opt;
    unsigned addr, size;
    while (fscanf(fp, " %c %x,%u", &opt, &addr, &size) > 0) {
        if (opt == 'I') continue;
        if (verbose) printf(" %c %x,%u", opt, addr, size);
        if (opt == 'M') access(addr), access(addr);
        else if (opt == 'L') access(addr);
        else if (opt == 'S') access(addr);
        if (verbose) printf("\n");
    }
    fclose(fp);
}

读取命令行参数

实验文档中建议我们使用 getopt 函数。

简单介绍一下,这个函数可以从命令行参数中一个个筛出有效的参数或者无效的信息:

int getopt(int argc,char * const argv[ ],const char * optstring);

这个函数的前两个参数和 main 函数的参数格式是相同的,分别是命令行参数的数量和命令行参数构成的字符串数组。第三个参数是命令选项,形如 ab:c:de,其中每一个字母 x 代表着接受一个 -x 的选项,字母后面接着 : 代表这个 -x 参数后面必须带有一个选项参数。

这个函数会依次扫描每一个命令行参数,如果不匹配我们提供的选项会输出报错,如果匹配了会返回匹配到的选项名(字符形式),同时在 optarg 这个全局字符指针中存储这个选项对应的参数。如果扫描结束,返回值会是 \(-1\)。

我们简单利用一下这个函数就可以成功读取命令啦!

bool verbose;
void printHelp(char *file);

int main(int argc, char **argv) {
    int s = -1, E = -1, b = -1;
    char opt;
    FILE *fp = NULL;
    while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
        switch (opt) {
            case 'h':
                printHelp(argv[0]);
                return 0;
            case 'v':
                verbose = 1;
                break;
            case 's':
                s = atoi(optarg);
                break;
            case 'E':
                E = atoi(optarg);
                break;
            case 'b':
                b = atoi(optarg);
                break;
            case 't':
                fp = fopen(optarg, "r");
                if (fp == NULL) {
                    printf("%s: No such file or directory\n", optarg);
                    return 0;
                }
                break;
            default:
                printHelp(argv[0]);
                return 0;
        }
    }
    if (!~s || !~E || !~b || !fp) {
        printf("Missing required command line argument\n");
        printHelp(argv[0]);
        return 0;
    }
    initCache(s, E, b);
    readTrace(fp);
    printSummary(hitc, missc, evic);
    return 0;
}

完整程序

完整程序太长啦,大家可以点击 这个链接 来下载!

贴一下得分:

./20230905-csapp-cachelab/image-20230906160756639

Part B: Optimizing Matrix Transpose

终于到 Part B 啦。Part B 的主要任务是优化一个矩阵转置的程序,使得它在一个 \(s = 5, E = 1, b = 5\) 的高速缓存上,不命中的次数最少。

这个部分实验主要想让我们学习一种叫作「分块」的技巧。具体的知识可以参考文档中提供的资料:http://csapp.cs.cmu.edu/public/waside/waside-blocking.pdf

这里贴一张书上给出的简单介绍以供参考。

./20230905-csapp-cachelab/image-20230906160357026

默认的矩阵转置代码,不做优化的是这个样子的:

void trans(int M, int N, int A[N][M], int B[M][N])
{
    int i, j, tmp;

    for (i = 0; i < N; i++) {
        for (j = 0; j < M; j++) {
            tmp = A[i][j];
            B[j][i] = tmp;
        }
    }    

}

一共分为 \(3\) 个子任务,每个子任务都需要达到一定的优化目标,具体的给分如下:

./20230905-csapp-cachelab/image-20230906160605505

\(32 \times 32\)

首先看一下如果不做优化的不命中次数是多少:

./20230905-csapp-cachelab/image-20230906160849497

很显然,这个距离我们的目标还挺远的。先来理解一下这个 \(1183\) 是怎么计算出来的吧。

首先我们很容易发现,\(A\) 和 \(B\) 两个矩阵的首地址末 \(10\) 位都是相同的,也就是说,这两个矩阵的相同位置一定会出现在相同的高速缓存组中。

一个高速缓存块是连续的 \(2^5 = 32\) 字节,也就是 \(8\) 个 int 类型的变量,每行 \(8\) 个这样的块,所以整个高速缓存可以储存 \(\frac {32}4 = 8\) 行的矩阵。因此,如果按照外层枚举行内层枚举列的顺序,可以最高效率地使用缓存,一般可以达到每 \(8\) 个元素才会出现一次不命中,不命中总次数应该是 \(32 \times 32 \div 8 = 128\) 次。这就是 \(A\) 矩阵的估计不命中次数。

但是,如果按照 \(B\) 矩阵的枚举顺序,外层枚举列,内层枚举行,那么每次访问都会产生不命中(因为每个缓存块的内容,只会被使用一次,在下次使用之前这个块早就被驱逐了),因此 \(B\) 矩阵的不命中次数是 \(32 \times 32 = 1024\) 次。那么加起来,我们的预估总不命中次数应该是 \(1024 + 128 = 1152\) 次。比实际的要少 \(31\) 次,这是什么原因呢?

实际上,因为 \(A\) 和 \(B\) 矩阵是同时被访问的,因此 \(B\) 矩阵的操作会影响 \(A\) 矩阵的缓存。这种影响只会发生在转置对角线元素的时候,只有这个时候 \(A\) 和 \(B\) 会使用同一个缓存,对 \(B\) 的写会把 \(A\) 当前的缓存驱逐出去,因此下一次读 \(A\) 上同一个块的时候,会触发一次不命中。只有整个矩阵最后一个元素在写完以后不需要再读这个块,因此总共多了 \(31\) 次不命中。

知道了不命中的来源,怎么优化也就很明显了。可以观察到,主要的不命中都是 \(B\) 矩阵贡献的。如果我们能让 \(B\) 矩阵的缓存发挥左右,就可以解决这个问题。

每次在 \(B\) 矩阵上写 \(8\) 行就会触发一次驱逐,因此我们可以最多同时写 \(8\) 个块。所以思路很简单:

将 \(A\) 和 \(B\) 每个矩阵分成 \(4\times 4\) 个大小为 \(8\times 8\) 的块,每次将 \(A\) 上的一个块转置到 \(B\) 上对应的块中,一个块处理完再去处理下一个块。这样保证了 \(B\) 的缓存都被写完了才会驱逐。

代码很简单:

void transpose_32x32(int M, int N, int A[N][M], int B[M][N]) {
    for (int ii = 0; ii < 32; ii += 8)
        for (int jj = 0; jj < 32; jj += 8)
            for (int i = ii; i < ii + 8; ++i)
                for (int j = jj; j < jj + 8; ++j)
                    B[j][i] = A[i][j];
}

然而很遗憾,这个做法的不命中数依然比 \(300\) 多:

./20230905-csapp-cachelab/image-20230906164612264

为什么会这样呢?注意到我们之前的朴素矩阵转置中,为什么不命中数会多了 \(31\)?那是因为对角线上的元素会导致 \(A\) 和 \(B\) 两个矩阵的同时访问,干扰缓存。而在使用了分块策略之后,这个问题就变得更加突出而复杂。对于对角线上的块,\(A\) 和 \(B\) 在读写的时候会不断地相互驱逐,对角线上的每个元素应该都会带来三次多余的驱逐:\(A\) 的每一行第一个元素的取用会驱逐 \(B\) 的这一行,对角线上 \(B\) 的写会驱逐 \(A\) 的这一行,\(A\) 继续读这一行的元素会驱逐 \(B\) 的这一行……

想要解决这个问题,可以使用 \(8\) 个局部变量来一次性取完 \(A\) 的一整行,再写入 \(B\) 的相应位置,这样就避免了两次驱逐:写 \(B\) 的对角线驱逐 \(A\) 和读 \(A\) 这一行之后的元素又驱逐 \(B\)。虽然无法彻底解决对角线块冲突的问题,但是应该可以做到每行只会冲突一次左右。

void transpose_32x32(int M, int N, int A[N][M], int B[M][N]) {
    for (int ii = 0; ii < 32; ii += 8)
        for (int jj = 0; jj < 32; jj += 8)
            for (int i = ii; i < ii + 8; ++i) {
                int a0 = A[i][jj];
                int a1 = A[i][jj + 1];
                int a2 = A[i][jj + 2];
                int a3 = A[i][jj + 3];
                int a4 = A[i][jj + 4];
                int a5 = A[i][jj + 5];
                int a6 = A[i][jj + 6];
                int a7 = A[i][jj + 7];
                B[jj][i] = a0;
                B[jj + 1][i] = a1;
                B[jj + 2][i] = a2;
                B[jj + 3][i] = a3;
                B[jj + 4][i] = a4;
                B[jj + 5][i] = a5;
                B[jj + 6][i] = a6;
                B[jj + 7][i] = a7;
            }
}

最终结果:

./20230905-csapp-cachelab/image-20230906170445416

\(64\times 64\)

\(64\times 64\) 的矩阵转置和前一个版本最大的区别就是,原本一行只有 \(4\) 个块,现在一行有了 \(8\) 个块,相应地,原本可以做到 \(8\) 行的不冲突,现在只能做到 \(4\) 行了。

这样就导致,原本 \(8\times 8\) 的分块是很天然的,现在天然的分块应该是 \(4\times 8\) 了,但是这样的分块不利于转置,因为转置只有在一个方方正正的块中才方便。

于是可以想到缩小为 \(4\times 4\) 的块。然而这样做虽然很简单,但是并不能获得满分。

那么我们可以怎么优化呢?

考虑到我们在用 \(4\times 4\) 的块进行转置的时候,其实缓存也会存放右边的 \(4\times 4\) 的块。如图,我们不妨将这块缓存利用起来。首先,我们将一个 \(8\times 8\) 的区域划分为 \(4\) 个 \(4\times 4\) 的小区域,在 \(A\) 矩阵中依次编号为 I, II, III, IV。

./20230905-csapp-cachelab/image-20230906172303893

在一开始,我们将 I 区域的转置做完,这个时候,\(A\) 的 II 区域和 \(B\) 矩阵的原本应该存放 III 的区域都已经进入缓存了。因此,我们可以利用这个缓存,将 \(A\) 矩阵的 II 区域的转置先放进 \(B\) 矩阵中原本应该放 III 的区域中,为了方便我们暂时称 \(B\) 的这个区域为 II' 区域。

./20230905-csapp-cachelab/image-20230906173422888

接下来,我们考虑如何将 B 中 II' 区域的一行放进 III 区域的同时,将 A 中 III 区域的转置正确放进 B 的 II' 区域,且不带来不必要的驱逐。如图,我们先将 II' 区域中的一小行复制到临时变量中,然后从 \(A\) 矩阵的 III 区域复制一列到 II‘ 区域的一小行中。复制完成后,我们在将临时变量中取出的一小行放到 B 真正的 II 区域的对应位置。这个过程中,II' 区域这一行将被驱逐,真正的 II 区域中这一行将被缓存,没有重复地驱逐再缓存再驱逐任何一行。

./20230905-csapp-cachelab/image-20230906173910783

结束后,就只剩下了 IV 区域还没有完成转置,而这个转置过程显然不难,和 I 区域相同的方法即可。

最终代码:

void transpose_64x64(int M, int N, int A[N][M], int B[M][N]) {
    int a0, a1, a2, a3, a4, a5, a6, a7;
    for (int ii = 0; ii < N; ii += 8)
        for (int jj = 0; jj < M; jj += 8) {
            for (int i = ii; i < ii + 4; ++i) {
                a0 = A[i][jj + 0];
                a1 = A[i][jj + 1];
                a2 = A[i][jj + 2];
                a3 = A[i][jj + 3];
                a4 = A[i][jj + 4];
                a5 = A[i][jj + 5];
                a6 = A[i][jj + 6];
                a7 = A[i][jj + 7];
                B[jj + 0][i] = a0;
                B[jj + 1][i] = a1;
                B[jj + 2][i] = a2;
                B[jj + 3][i] = a3;
                B[jj + 0][i + 4] = a4;
                B[jj + 1][i + 4] = a5;
                B[jj + 2][i + 4] = a6;
                B[jj + 3][i + 4] = a7;
            }
            for (int j = jj; j < jj + 4; ++j) {
                a0 = B[j][ii + 4];
                a1 = B[j][ii + 5];
                a2 = B[j][ii + 6];
                a3 = B[j][ii + 7];
                B[j][ii + 4] = A[ii + 4][j];
                B[j][ii + 5] = A[ii + 5][j];
                B[j][ii + 6] = A[ii + 6][j];
                B[j][ii + 7] = A[ii + 7][j];
                B[j + 4][ii + 0] = a0;
                B[j + 4][ii + 1] = a1;
                B[j + 4][ii + 2] = a2;
                B[j + 4][ii + 3] = a3;
            }
            for (int i = ii + 4; i < ii + 8; ++i) {
                a0 = A[i][jj + 4];
                a1 = A[i][jj + 5];
                a2 = A[i][jj + 6];
                a3 = A[i][jj + 7];
                B[jj + 4][i] = a0;
                B[jj + 5][i] = a1;
                B[jj + 6][i] = a2;
                B[jj + 7][i] = a3;
            }
        }
}

./20230905-csapp-cachelab/image-20230906174114851

最终的不命中数是 \(1227\),可以通关了。

\(61 \times 67\)

最后一题是 \(61\) 和 \(67\),因为不是方方正正的正方形,所以很难直接分析分块的方法。

所幸这个任务的限制很宽松,很多方法都可以通过这个题目。

一种很有用的方法是直接枚举块大小,每一种都试一遍,很快就能找到可以通过的程序了。

我是直接在 \(8\times 8\) 分块的版本上修改了一下对角线处理的版本,加了边界判断,然后就可以通过要求了。

void transpose_61x67(int M, int N, int A[N][M], int B[M][N]) {
    int a0, a1, a2, a3, a4, a5, a6, a7;
    for (int ii = 0; ii < N; ii += 8)
        for (int jj = 0; jj < M; jj += 8)
            for (int i = ii; i < ii + 8 && i < N; ++i) {
                if (jj + 0 < M) a0 = A[i][jj];
                if (jj + 1 < M) a1 = A[i][jj + 1];
                if (jj + 2 < M) a2 = A[i][jj + 2];
                if (jj + 3 < M) a3 = A[i][jj + 3];
                if (jj + 4 < M) a4 = A[i][jj + 4];
                if (jj + 5 < M) a5 = A[i][jj + 5];
                if (jj + 6 < M) a6 = A[i][jj + 6];
                if (jj + 7 < M) a7 = A[i][jj + 7];
                if (jj + 0 < M) B[jj][i] = a0;
                if (jj + 1 < M) B[jj + 1][i] = a1;
                if (jj + 2 < M) B[jj + 2][i] = a2;
                if (jj + 3 < M) B[jj + 3][i] = a3;
                if (jj + 4 < M) B[jj + 4][i] = a4;
                if (jj + 5 < M) B[jj + 5][i] = a5;
                if (jj + 6 < M) B[jj + 6][i] = a6;
                if (jj + 7 < M) B[jj + 7][i] = a7;
            }
}

./20230905-csapp-cachelab/image-20230906174552554

成功贴着线过了这个任务。ヾ(ヾ)

程序

程序太长了就不直接贴上来了,可以点击这个 链接 下载!

得分

贴一下总分的图。

./20230905-csapp-cachelab/image-20230906174749839

标签:CSAPP,int,Cache,矩阵,Lab,times,ii,jj,高速缓存
From: https://www.cnblogs.com/hankeke303/p/18155096/csapp-cachelab

相关文章

  • CSAPP Lab6 Shell Lab
    本次实验的任务很清晰,实现一个简单的UnixShell。需要用到基础的进程控制、信号处理等知识。简单来说,实验已经提供了一些简单的功能,我们需要在此基础上,实现下面的功能:eval:解析和解释命令行的主例程。[70行]builtin_cmd:识别并解释内置命令quit(退出)、fg(前台运行某个作业)、bg(后......
  • CSAPP Lab-2 BOMBLAB
    第二个Lab就比较有趣了。这一个Lab的任务是,我们得到了一个bomb炸弹程序,这个炸弹程序有\(6\)个phase,每个phase都会读取我们的输入,判断我们的输入是否符合要求,如果正确这个phase的炸弹就会被拆除,否则炸弹就会爆炸。我们需要借助各种工具,对程序进行反汇编等等,获得能够......
  • 伯克利大学 CS61B Lab配置教程
    基本过程:首先将伯克利大学的代码框架下载到自己的电脑,然后我们直接在框架里修改就行将自己的代码上传到github上,然后使用伯克利大学的Gradescope评测自己写的代码下载代码在自己电脑桌面新建一个文件夹,这里我命名为:cs61b,打开gitbash,使用cd进入我们新创建的文件夹,注意路径......
  • 读《我和Labview》5条件结构和顺序结构
    5条件结构和顺序结构条件结构布尔类型条件选择结构其它数据类型的条件选择是否要设置默认分支?合理设置悬着条件隧道避免把控件放在条件结构内选择函数顺序结构程序执行顺序创建顺序结构层叠式顺序结构平铺式顺序结构无形胜有形的最高境界6用户自定义控件7控件的局......
  • pwn知识——劫持__malloc_hook(在加入tcache以后)
    导论动调是最好的导师!malloc_hook函数解析malloc_hook是malloc的钩子函数,在执行malloc时,会先检测__malloc_hook的值,如果malloc_hook的值存在,则执行该地址(值里边表现为十六进制,可以成为地址),也就是说,如果我们成功劫持malloc_hook以后并修改它的值为one_gadget,我们就能getshell并......
  • Linux服务器中Docker部署的GitLab镜像访问出现500错误
    一背景这几天发现在Linux服务器中Docker部署的GitLab镜像访问出现500错误,在重启服务器后大概10分钟再次出现该情况,后面登录服务器一步步排查最终解决问题,现在将解决问题的过程做一个总结。二过程分析首先第一步就是看看我们Docker目录下文件占用的情况,因为我们的Linux服务......
  • 如何将Docker中GitLab数据备份到宿主Linux上
    一宿主机准备存放备份文件的目录建议以年月日进行命名使用putty.exe或者PowerShell登录远程服务器cdshare(如果没有当前目录请创建该共享目录)mkdir20220930(在共享目录下创建备份文件夹)二进入Docker容器内部备份数据1.执行命令sudodockerexec-itgitlab/bin/......
  • feign调用接口报错No qualifying bean of type '***HttpMessageConverters' available
    在整合springcloudgeateway时,调用了feign接口,报错Noqualifyingbeanoftype'org.springframework.boot.autoconfigure.http.HttpMessageConverters'available报错信息feign.codec.EncodeException:Noqualifyingbeanoftype'org.springframework.boot.autocon......
  • MIT6.S081 - Lab2: system calls
    Lab2:systemcalls预备知识执行一次系统调用的流程:USERMODEstep1:系统调用声明user/user.h:系统调用函数(如intfork(void))step2:ecall进入内核态user/usys.S(该文件由user/usys.pl生成,后续添加函数可以在这里添加):执行如下命令.globalforkfork:lia7,SYS_f......
  • 读《我和Labview》3.5-3.6路径和数据平化
    3.5路径3.5.1路径数据3.5.2相对路径3.5.3路径常量3.5.4路径与其他数据类型的转换3.6数据平化3.6.1数据平化至字符串3.6.2数据平化至XML3.6.3数据平化至JSON4图形化显示数据5条件结构和顺序结构6用户自定义控件7控件的局部变量和属性8按自己的喜好设置编程环境......