计算机系统概论 作业3


  1. 编程解决猴子吃桃问题: 每天吃一半再多吃一个, 第十天想吃时候只剩一个, 问总共有多少. 该程序的 $C$ 语言程序如下, 请在其对应汇编代码 ($Linux X86- 64$) 内填入缺失内容.

    int eat_peaches(int i) { 
    	if (i == 10) { 
    		return 1; 
    	} else { 
    		return (eat_peaches(i + 1) + 1) * 2; 
    	}
    }
    eat_peaches:
        cmpl $10, __①__
        je   __②__
        subq $8, %rsp
        addl $1, %edi
        call eat_peaches
        leal 2(%rax, __③__), %eax
        jmp  __④__
    .L3:
    	movl $1, %eax __⑤__ 
    
    .L2:
        addq __⑥__, %rsp
        ret
    : %edi
    ②: .L3
    ③: %rax
    ④: .L2
    ⑤: ret
    ⑥: $8
  2. 有下列 $C$ 代码以及对应的汇编代码 ($Linux X86-64$), 请填充下表, 即给出各个变量或者寄存器在栈中的存储位置 (以相对于栈帧基址寄存器 %rbp​ 的十进制偏移量形式给出, 可正可负); 如果无法以 “在栈中的存储位置” 形式给出, 请说明理由.

    void foo(int x) {
    	int a[3];
    	char buf[4];
    	a[0] = 0xF0F1F2F3;
    	a[1] = x;
    	gets(buf);
    	printf("a[0] = 0x%x, a[1] = 0x%x, buf = %s\n", 
    	a[0], a[1], buf);
    }
    .LC0:
    	.string "a[0] = 0x%x, a[1] = 0x%x, buf = %s\n"
    foo:
        pushq %rbp
        movq  %rsp, %rbp
        pushq %rbx
        subq  $24, %rsp
        movl  %edi, %ebx
        leaq  -32(%rbp), %rdi
        movl  $0, %eax
        call  gets
        leaq  -32(%rbp), %rcx
        movl  %ebx, %edx
        movl  $-252579085, %esi
        movl  $.LC0, %edi
        movl  $0, %eax
        call  printf
        addq  $24, %rsp
        popq  %rbx
        popq  %rbp
        ret
    | 变量 | 十进制形式的 $offset$ (或者说明) | | :-------------: | :------------------------------: | | `a` | 经过优化, 未在栈帧中存储 | | `a[2]` | 经过优化, 未在栈帧中存储 | | `x` | 存储在 `%rbx` 中, 未在栈帧中存储 | | `buf` | $-32$ | | `buf[3]` | $-29$ | | `%rbx` 的保存值 | $-8$ |
  3. 过程调用以及返回的顺序在一般情况下都是 “过程返回的顺序恰好与调用顺序相反”, 但是我们可以利用汇编以及对运行栈的理解来编写汇编过程打破这一惯例.

    有如下汇编代码 ( $x86-32$ 架构 ), 其中 GET 过程唯一的输入参数是一个用于存储当前处理器以及栈信息的内存块地址 (假设该内存块的空间足够大), 而 SET 过程则用于恢复被 GET 过程所保存的处理器及栈信息, 其唯一的输入参数也是该内存块地址. 在理解代码的基础上, 回答下列问题:

    GET:
        movl 4(%esp), %eax     #(A)
        …
        movl %edi, 20(%eax)
        movl %esi, 24(%eax)
        movl %ebp, 28(%eax)
        movl %ebx, 36(%eax)
        movl %edx, 40(%eax)
        movl %ecx, 44(%eax)
        movl $1, 48(%eax) 
        movl (%esp), %ecx      #(B)
        movl %ecx, 60(%eax)
        leal 4(%esp), %ecx     #(C)
        movl %ecx, 72(%eax)
        movl 44(%eax), %ecx
        movl $0, %eax
        ret
    SET:
        movl 4(%esp), %eax
        …
        movl 20(%eax), %edi
        movl 24(%eax), %esi
        movl 28(%eax), %ebp
        movl 36(%eax), %ebx
        movl 40(%eax), %edx
        movl 44(%eax), %ecx
        movl ____ (%eax), %esp #(D)
        pushl 60(%eax)         #(E)
        movl 48(%eax), %eax
        ret

    (1) SET​ 过程的返回地址是什么, 其返回值是多少?

    解答:

    返回地址为 GET 函数的下一条地址; 返回值为 $1$.

    (2) 代码段中的 $(A)$ 指令执行后,eax 中存放的是什么? $(B)$ 指令执行后, ecx 中存放的是什么? $(C)$ 指令的作用是什么? $(E)$ 指令的作用是什么? 并将 $(D)$ 指令补充完整.

    解答:

    $(A)$ 指令执行后, eax​ 中存放的是用于存储处理器与栈信息的内存块的地址, 即 GET 函数的输入参数;

    $(B)$ 指令执行后, ecx​ 中存放的是 GET 函数的返回地址;

    $(C)$ 指令的作用是临时存储 4(%rsp) 的内容, 并中转至目标内存中;

    $(E)$ 指令的作用是将之前记录的 GET 函数的返回地址压入栈, ret 指令跳转到之前的相应地址;

    $(D)$ 指令需填入 $72$.

  4. 请分别对照下列的 $C$ 代码与对应的汇编代码, 解释下 $C$ 函数是如何传入 struct​ 类型参数的, 可以通过画出 call input_struct 时栈的 $layout$, 并辅以说明来解释.

    1) gcc –Og ...​

    typedef struct{
    	int age; int bye; int coo; int ddd; int eee;
    } TEST_Struct;
    int i = 2;
    int input_struct(TEST_Struct in_struct) {
    	return in_struct.eee + in_struct.age * 2 ; 
    }
    int function2() {
    	TEST_Struct main_struct;
    	main_struct.age = i;
    	main_struct.bye = i;
    	main_struct.coo = 2 * i;
    	main_struct.ddd = i;
    	main_struct.eee = i;
    	return input_struct(main_struct);
    }
    input_struct:
        movl 8(%rsp), %eax    #age
        addl %eax, %eax
        addl 24(%rsp), %eax   #eee
        ret
    function2:
        subq $56, %rsp
        movl i(%rip), %eax
        movl %eax, 24(%rsp)   #age
        movl %eax, 28(%rsp)   #bye
        leal (%rax,%rax), %edx
        movl %edx, 32(%rsp)   #coo
        movl %eax, 36(%rsp)   #ddd
        movq 24(%rsp), %rdx
        movq %rdx, (%rsp)     #age/bye
        movq 32(%rsp), %rdx
        movq %rdx, 8(%rsp)    #coo/ddd 
        movl %eax, 16(%rsp)   #eee
        call input_struct
        addq $56, %rsp
        ret

    解答:

    在栈帧中存储结构体实例. 在发生调用 call input_struct​ 前, 在 24(%rsp) 开始由低地址向高地址存储 age, bye, coo, ddd. 随后将这些信息转移到 %rsp 开始的低地址内存中, 并附加 eee.

    调用 call input_struct 后, 返回地址被压入栈中, 此时通过 8(%rsp) 访问的即是 in_struct.age, 通过 24(%rsp) 访问的即是 in_struct.eee.

    2) $C$ 代码不变, 通过 gcc -O1/2 ... 编译后的汇编如下:

    input_struct:
        movl 24(%rsp), %eax
        movl 8(%rsp), %edx
        leal (%rax,%rdx,2), %eax
        ret
    function2:
        movl i(%rip), %eax
        leal (%rax,%rax,2), %eax
        ret

    请分析针对这段代码, 编译器做了什么优化工作.

    解答:

    使用一步 leal 指令替代原本的两步 addl 指令;

    直接使用全局变量 i 代替已知相等的 in_struct.agein_struct.age 进行计算.

    3) 如果在上面的 $C$ 代码的 int​ input_struct 声明前加上 ​static, 经 gcc –O1/2 ... 编译后的代码如下:

    function2:
        movl i(%rip), %eax
        leal (%rax,%rax,2), %eax
        ret
    

    请分析针对这段代码, 编译器做了什么优化工作.

    解答:

    int input_struct 声明为 static 后只会被当前编译单元被调用, 优化 function2 后无需调用 input_struct, 因此可以不进行编译.

  5. 有如下三类结构、联合定义, 请根据左侧的汇编语言 $(x86-32)$, 补齐右侧的 $C$ 语言.

    struct s1 {
        char a[3];
        union u1 b;
        int c;
    };
    
    struct s2 {
        struct s1 *d;
        char e;
        int f[4];
        struct s2 *g;
    };
    
    union u1 {
        struct s1 *h;
        struct s2 *i;
        char j;
    };
    A.proc1:
    	pushl %ebp
    	movl %esp, %ebp
    	movl 8(%ebp), %eax
    	movl 12(%eax), %eax
    	movl %ebp, %esp
    	popl %ebp
    	ret
    int proc1(struct s2 *x) {
        return x-> __________;
    }
    B.proc2:
    	pushl %ebp
    	movl %esp, %ebp
    	movl 8(%ebp), %eax
    	movl 4(%eax), %eax
    	movl 20(%eax), %eax
    	movl %ebp, %esp
    	popl %ebp
    	ret
    int proc2(struct s1 *x) {
        return x-> __________;
    }
    C.proc3:
    	pushl %ebp
    	movl %esp, %ebp
    	movl 8(%ebp), %eax
    	movl (%eax), %eax
    	movsbl 4(%eax), %eax
    	movl %ebp, %esp
    	popl %ebp
    	ret
    char proc3(union u1 *x) {
        return x-> __________;
    }
    D.proc4:
    	pushl %ebp
    	movl %esp, %ebp
    	movl 8(%ebp), %eax
    	movl (%eax), %eax
    	movl 24(%eax), %eax
    	movl (%eax), %eax
    	movsbl 1(%eax), %eax
    	movl %ebp, %esp
    	popl %ebp
    	ret
    char proc4(union u1 *x) {
        return x-> __________;
    }
    A: f[1] 
    B: b.i->f[3]
    C: i->e
    D: i->g->d->a[1]
  6. 已知以下 $C++$ 代码与对应的运行结果, 对源代码进行补全并绘制 struct A​ 各变量在内存中的存放位置.

    #include <iostream>
    #include <cstdint>
    using namespace std;
    struct A {
    	T1 a;
    	T2 b;
    	struct B {
    		T3 c;
    		T4 d;
    	};
    	B e[N];
    };
    int main() {
        A s;
        size_t size_A = sizeof(s);
        std::cout << "size of A:" << size_A << std::endl;
        unsigned char* p = (unsigned char*) &s;
        for (int i = 0; i < size_A; i++) p[i] = 0xaa;
        std::cout << "a: " << s.a << std::endl;
        std::cout << "b: " << s.b << std::endl;
        std::cout << "e[0].c: " << s.e[0].c << std::endl;
        std::cout << "e[0].d: " << s.e[0].d << std::endl;
        return 0;
    }

    运行结果:

    size of A: 96
    a: -21846
    b: 12297829382473034410
    e[0].c: -1431655766
    e[0].d: -3.72066e-103

    请补全以下类型和常数:

    T1: short
    T2: unsigned long long
    T3: int
    T4: double
    N: 5

    struct A​ 的内存布局 (需绘制出 struct B 中各变量):


文章作者: Chengsx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chengsx !
  目录