参考资料
信息的表示和处理
大小端的思考
下面的代码表示大小端的区别,这段代码通过转字符串的方式,强制将数字类型转换为字符类型,显示了其数字在该小端法机器中的存储顺序。
#include <stdio.h> typedef unsigned char * byte_pointer;void show_bytes (byte_pointer start, size_t len) { size_t i; for (i = 0 ; i < len; i++) { printf ("%.2x " , start[i]); } printf ("\n" ); } void show_int (int x) { show_bytes ((byte_pointer)&x, sizeof (int )); } void show_float (float x) { show_bytes ((byte_pointer)&x, sizeof (float )); } void show_pointer (void *x) { show_bytes ((byte_pointer)&x, sizeof (void *)); } void test_show_bytes (int val) { int ival = val; float fval = (float )ival; int *pval = &ival; show_int (ival); show_float (fval); show_pointer (pval); } int main () { test_show_bytes (12345 ); return 0 ; } 39 30 00 00 00 e4 40 46 d4 fd 61 00 00 00 00 00
思考一下,现在的机器大多数比较统一,基本采用的都是小端法,但是也存在大端法的显示,在不同机器上运行的时候需要考虑。
Swap
下面这个交换函数是没有性能优化的,但是省去了中间变量,是怎么做到的?
void inpalce_swap (int *x, int *y) { *y = *x ^ *y; *x = *x ^ *y; *y = *x ^ *y; }
我专门测了一下时间,确实没有优化,但还是挺神奇的。
#include <stdio.h> #include <time.h> void inpalce_swap (int *x, int *y) { *y = *x ^ *y; *x = *x ^ *y; *y = *x ^ *y; } void swap (int *x, int *y) { int mid = 0 ; mid = *x; *x = *y; *y = mid; } clock_t start1, stop1;clock_t start2, stop2;double duration1;double duration2;int main () { int a = 3 , b = 4 ; int c = 8 , d = 9 ; long long n = 1000000001 ; printf ("%d,%d |" , a, b); printf ("%d,%d\n" , c, d); start1 = clock (); for (int i = 0 ; i < n; i++) { inpalce_swap (&a, &b); } stop1 = clock (); duration1 = ((double )(stop1 - start1)) / CLK_TCK; printf ("%d,%d\n" , a, b); printf ("%f\n" , duration1); start2 = clock (); for (int i = 0 ; i < n; i++) { swap (&c, &d); } stop2 = clock (); duration2 = ((double )(stop2 - start2)) / CLK_TCK; printf ("%d,%d\n" , c, d); printf ("%f\n" , duration2); return 0 ; } 3 ,4 |8 ,9 4 ,3 0.280000 9 ,8 0.274000
移位运算
#include <bitset> #include <iostream> using namespace std;int main () { unsigned short a = 0b0001 ; bitset<16> b1 (a) ; cout << b1 << " " << a << endl; unsigned short b = a << 2 ; bitset<16> b2 (b) ; cout << b2 << " " << b << endl; unsigned short sum = a << 1 + b << 1 ; bitset<16> b3 (sum) ; cout << b3 << " " << sum << endl; return 0 ; } 0000000000000001 1 0000000000000100 4 0000000001000000 64
整形
#include <bitset> #include <iostream> using namespace std;int main () { int64_t a = 9223372036854775807 ; uint64_t b = 18446744073709551615 ; cout << sizeof (int64_t ) << " " << sizeof (long long ) << " " << a << endl; cout << sizeof (uint64_t ) << " " << b; return 0 ; } 8 8 9223372036854775807 8 18446744073709551615
浮点数部分
这部分计组有所涉猎,暂时不详读。
程序的机器级表示
汇编和机器码
对于如下代码
#include <stdio.h> long mult2 (long ,long ) ;void mulstore (long x,long y, long *dest) { long t = mult2 (x,y); *dest =t; } int main () {return 0 ;}
可以用
生成,汇编代码如下mstore.s 文件
#mstore. s 文件 .file "mstore.c" .text .globl mulstore .type mulstore, @functionmulstore: .LFB23: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3 , -16 movq %rdx, %rbx call mult2@PLT movq %rax, (%rbx) popq %rbx .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE23: .size mulstore, .-mulstore .globl main .type main, @functionmain: .LFB24: .cfi_startproc movl $0 , %eax ret .cfi_endproc .LFE24: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note. GNU-stack,"" ,@progbits
可以用,以点开头的指令通常为伪指令,指导汇编器和链接器工作
生成对应的二进制机器码,是不可读的乱码
Linux中的反汇编器为,可以将二进制的机器码反汇编为汇编代码
$ objdump -d mstore.o mstore.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <mulstore>: 0: 53 push %rbx 1: 48 89 d3 mov %rdx,%rbx 4: e8 00 00 00 00 callq 9 <mulstore+0x9> 9: 48 89 03 mov %rax,(%rbx) c: 5b pop %rbx d: c3 retq 000000000000000e <main>: e: b8 00 00 00 00 mov $0x0,%eax 13: c3 retq
可以看到上面的代码与之前有所不同,在于命令后面的q被省去了,有的添加了q,q表示大小指示符,是可以省略的。
使用
$ gcc -Og -S -masm=intel mstore.c
则可以显示Intel代码,省略了一些不必要的标识符
.file "mstore.c" .intel_syntax noprefix .text .globl mulstore .type mulstore, @functionmulstore: .LFB23: .cfi_startproc push rbx #入栈 .cfi_def_cfa_offset 16 .cfi_offset 3 , -16 mov rbx , rdx call mult2@PLT mov QWORD PTR [rbx ], rax pop rbx #出栈 .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE23: .size mulstore, .-mulstore .globl main .type main, @functionmain: .LFB24: .cfi_startproc mov eax , 0 ret .cfi_endproc .LFE24: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note. GNU-stack,"" ,@progbits
循环操作
最近在学汇编的循环,顺便实验一下在gcc下生成的汇编代码
#include <stdio.h> int main () { int a =0 ; while (a<10 ){ a++; } printf ("%d" ,a); return 0 ; }
汇编代码如下
.file "mstore.c" .intel_syntax noprefix .text .section .rodata. str1.1 ,"aMS" ,@progbits,1 .LC0: .string "%d" .text .globl main .type main, @functionmain: .LFB23: .cfi_startproc sub rsp , 8 .cfi_def_cfa_offset 16 mov edx , 0 jmp .L2 #跳到转判断 .L3: add edx , 1 #循环体内语句 .L2: cmp edx , 9 #判断 jle .L3 #跳转回循环体 lea rsi , .LC0[rip ] mov edi , 1 mov eax , 0 call __printf_chk@PLT mov eax , 0 add rsp , 8 .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE23: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note. GNU-stack,"" ,@progbits
移位操作
long shift_left_rightn (long x,long n) { x<<=4 ; x>>=n; return x; } int main () { return 0 ; }
这里没有使用intel用的是asm
.file "mstore.c" .text .globl shift_left_rightn .type shift_left_rightn, @functionshift_left_rightn: .LFB23: .cfi_startproc movq %rdi, %rax salq $4 , %rax #这里是移位的语句 movl %esi, %ecx sarq %cl, %rax #这里是移位的语句 ret .cfi_endproc .LFE23: .size shift_left_rightn, .-shift_left_rightn .globl main .type main, @functionmain: .LFB24: .cfi_startproc movl $0 , %eax ret .cfi_endproc .LFE24: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note. GNU-stack,"" ,@progbits
GDB使用
理解指针
#include <stdio.h> long shift_left_rightn (long x,long n) { int *a = &x; *a = 1 ; return x; } int main () { return 0 ; }
汇编代码如下,看了很多汇编的代码之后,就可以看出一个函数都有LFB 和LFE 两个块,所有的注释都会被省略。
.intel_syntax noprefix .text .globl shift_left_rightn .type shift_left_rightn, @functionshift_left_rightn: .LFB23: .cfi_startproc mov QWORD PTR -8 [rsp ], rdi mov DWORD PTR -8 [rsp ], 1 mov rax , QWORD PTR -8 [rsp ] ret .cfi_endproc .LFE23: .size shift_left_rightn, .-shift_left_rightn .globl main .type main, @functionmain: .LFB24: .cfi_startproc mov eax , 0 ret .cfi_endproc .LFE24: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note. GNU-stack,"" ,@progbits
汇编语法部分
这部分暂时不看,因为学校的80x86ams的汇编还没有学完,学完后再看理解更深。
函数指针
函数指针定义的时候要使得其外部的函数的参数和返回类型均一致,如下:
#include <stdio.h> long add (long x,long n) { x = x+n; return x; } int main () { int a = 1 ,b=1 ; long (*f)(long x,long n) = add; a = (*f)(a,b); printf ("%d" ,a); return 0 ; }
有高人指点将上述代码改为, 也可运行
#include <stdio.h> long add (int x, int n) { return x + n; } int main () { printf ("Hello World\n" ); typedef long function_t (int , int ) ; function_t (*test_add) = add; long value = test_add (0x41 , 0x42 ); printf ("0x%lx\n" , value); return 0 ; }
内存越界引用和缓冲区溢出
这里要先了解一下函数调用栈
参考:https://z.itpub.net/article/detail/50503CAA1CDDA808A925D5758BD1B0A4
下面是一个缓冲区越界的例子,通常c中的很多库函数并没有指定缓冲区的大小,就会存在缓冲区buff(就是中间变量)越界的情况,导致栈空间的其他数据被破坏。
#include <stdio.h> char * gets (char *s) { int c; char *dest = s; while ((c = getchar ()) != '\n' && c != EOF) { *dest++ = c; } if (c == EOF && dest == s ) { return NULL ; } *dest++ = '\0' ; return s; } void echo () { char buf[8 ]; gets (buf); puts (buf); } int main () { echo (); return 0 ; }
echo: .LFB24: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3 , -16 subq $16 , %rsp .cfi_def_cfa_offset 32 movq %fs:40 , %rax movq %rax, 8 (%rsp) xorl %eax, %eax movq %rsp, %rbx movq %rbx, %rdi call gets movq %rbx, %rdi call puts@PLT movq 8 (%rsp), %rax xorq %fs:40 , %rax jne .L11 addq $16 , %rsp .cfi_remember_state .cfi_def_cfa_offset 16 popq %rbx .cfi_def_cfa_offset 8 ret .L11: .cfi_restore_state call __stack_chk_fail@PLT .cfi_endproc .LFE24: .size echo, .-echo .globl main .type main, @function
更常见的是使用fgets函数,指定了输出的边界,不会导致越界
#define N 101 char str[N];FILE *fp = fopen ("D:\\demo.txt" , "r" ); fgets (str, N, fp);
但是类似gets的函数有很多,在实际使用中要小心。这里和攻击代码有关,著名的蠕虫病毒就是其中的一个例子,通过缓冲区溢出来返回执行自己的攻击代码,在将其复制传播。
对抗缓冲区溢出攻击
栈随机化
攻击者既要插入代码,也要插入指向这段代码的指针,指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。
栈地址过去很容易预测,不同机器之间栈位置是相当固定的,容易导致攻击者可以攻击许多机器。
栈随机化的目的就是使得栈的位置在程序每次运行时都有变化。
实现方式:
程序开始时分配一段0~n字节之间的随机大小的空间,(使用分配函数alloca在栈上分配指定字节数量的空间)
程序不会使用这段空间,但是它会导致程序每次执行时后续的栈位置发生变化。
分配的范围必须足够大,才能获得足够多的栈地址变化,但是又需要小不至于浪费空间。
具体表现在Linux中,就是变量的地址在多次执行后会在一个范围内不断变化,这类技术叫做地址空间布局随机化ASLR。
#include <stdio.h> int main () { long local; printf ("local at %p\n" ,&local); return 0 ; }
touch 一个文件 + 一个参数即可,chmod +x 为可执行文件
# !/bin/bash echo "---------start shell---" for((i =1;i <= $1; i++)) do ./a.out $item done
栈随机化能增加黑客攻击的难度,但是黑客依然可以通过no op操作来滑过从而找到实际的程序的起始地址。
栈破坏检测
当函数超越局部缓冲区边界时,就说明栈已经被破坏。在c没有可靠的方法组织对数组边界写。但是我们能够在发生越界写的时候,检测到
gcc加入了栈保护机制
思想
在栈帧的中任何局部缓冲区与栈状态之间存储一个特殊的随机的金丝雀值 ,也称为哨兵值。
如果该值改变了,说明程序被破坏,异常终止
现在的做法通常将金丝雀的值放到一个只读的空间中,这样就可以在程序结束后做对比。
限制可执行代码区域
栈可以标记可读和可写
但是会带来性能损失
AMD使用了NX位,来标记执不执行
处理器体系结构
最近在学计组,这部分就暂时不看了
进程控制块PCB
这是我在Linux0.01的源码中,找到的与进程控制块相关的数据结构,PCB通常指的就是这样数据结构的数据
struct task_struct { long state; long counter; long priority; long signal; fn_ptr sig_restorer; fn_ptr sig_fn[32 ]; int exit_code; unsigned long end_code,end_data,brk,start_stack; long pid,father,pgrp,session,leader; unsigned short uid,euid,suid; unsigned short gid,egid,sgid; long alarm; long utime,stime,cutime,cstime,start_time; unsigned short used_math; int tty; unsigned short umask; struct m_inode * pwd ; struct m_inode * root ; unsigned long close_on_exec; struct file * filp [NR_OPEN ]; struct desc_struct ldt [3]; struct tss_struct tss ; };
优化程序性能
如何编写高效程序
选择一组适当的算法和数据结构
编写编译器能够有效优化,从而转化成高效可执行代码的源代码
针对处理运算量特别大的计算,将一个任务分成多个任务
在做了很多优化后,依然要尽量保持程序的可读性和模块性
如何进行程序优化
消除不必要的工作
利用处理器提供的指令集并行能力,同时执行多条指令
内存别名使用
上述代码中的-Og是在调用gcc并使用一组基本优化,打开优化标志会使编译器尝试以牺牲编译时间和调试程序的能力为代价来提高性能和/或代码大小。
在编译器不知道代码的特殊情况下,只能考虑最保守的情况
#include <stdio.h> void swap (long *xp,long *yp) { *xp = *xp + *yp; *yp = *xp - *yp; *xp = *xp - *yp; }
如果调用时xp等于yp,会有什么效果?
函数调用
long f () long func1 () { return f ()+f ()+f ()+f (); } long func2 () { return 4 *f (); }
就是这些是编译器优化容易错误的两个种情况,就是自己在写代码的时候优化应该更谨慎
long counter = 0 ;long func1in () { long t = counter++; t += counter++; t += counter++; t += counter++; return t; } long func1opt () { long t = 4 * counter + 6 ; counter += 4 ; return t; }
循环展开
void psum1 (float a[],float p[],long n) { long i; p[0 ] = a[0 ]; for (int i = 1 ; i < n; i++) { p[i] = p[i - 1 ] + a[i]; } } void psum2 (float a[],float p[],long n) { long i; p[0 ] = a[0 ]; for (i = 1 ; i < n - 1 ;i+=2 ) { float mid_val = p[i - 1 ] + a[i]; p[i] = mid_val; p[i + 1 ] = mid_val + a[i + 1 ]; } }
循环低效率消除
#define OP + #define IDENT 0 void combine1 (vec_ptr v,data_t *dest) { long i; *dest = IDENT; for (i = 0 ;i < vec_length (v) ;i++) { data_t val; get_vec_element (v,i,&val); *dest = *dest OP val; } } void combine2 (vec_ptr v,data_t *dest) { long i; long length = vec_length (v); *dest = IDENT; for (i = 0 ;i < length ;i++) { data_t val; get_vec_element (v,i,&val); *dest = *dest OP val; } }
存储层次结构
链接
链接是将各种代码收集组合成为单一文件的过程,可以在编译时链接也可也在加载时、甚至是运行时链接。
首先有以下文件
#include <stdio.h> int sum (int *a ,int n) ;int array [2 ] = {1 ,2 };int main () { int val = sum(array ,2 ); printf ("sum = %d" ,val); return val; }
int sum (int *a, int n) { int i,s=0 ; for (i = 0 ; i < n ; i++){ s +=a[i]; } return s; }
在Linux下使用gcc完成编译和链接
$ gcc -Og -o prog mian.c sum.c
添加-v 参数可以看到链接的全过程:
首先C预处理器(cpp),将原程序翻译为一个ASCII码中间文件main.i
驱动程序运行cc1,将main.i翻译为一个ASCII汇编语言文件main.s
驱动程序运行汇编器(as), 将main.s翻译成一个可重定位目标文件 main.o
驱动再次经过上述过程产生sum.o
运行链接器ld,将main.o和sum.o以及一些必要的系统目标文件组合起来,创建一个可执行文件 prog(-o 文件名定义,默认a.out)
在C++和java中,链接器的符号的重整,使得函数可以被重载,其内部会添加其他字符来区分。
可执行文件
加载器的工作原理