1 of 38

How C code is compiled

Compile

C++语言程序设计

2 of 38

Stack Segment

void A()

{ int a;

short b[4];

double c;

B();

C();

}

void B()

{ int x;

char *y;

char *z[2];

C();

}

viod C()

{ double m[3];

int n;

}

a

b

c

x

y

z

m

n

activation record

stack frame

stack pointer

Hardware

C++语言程序设计

3 of 38

Assembly Code

A

L

U

Arithmetic Logic Unit

Register

RAM

Stack

Heap

Code

32个

Registers are between ALU and RAM. Directly connecting ALU and RAM will make the hardware complicated and increase clock cycle.

R1

R2

R3

7

10

j

i

7

10

17

j+=i

CPU

C++语言程序设计

4 of 38

int i;

int j;

i = 10;

j = i + 7;

j++;

M[R1+4] = 10

R2 = M[R1+4]

R3 = R2 + 7

M[R1] = R3

R2 = M[R1]

R2 = R2 + 1

M[R1] = R2

Stack

10

17

j

i

R1

base address of the activation record

M: memory

//load operation

//ALU operation

//store operation

all 4-byte data

每条语句对应一组指令、上下文无关、独立

//store operation

18

C++语言程序设计

5 of 38

int i;

short s1;

short s2;

i = 200;

s1 = i;

s2=s1+1;

M[R1+4] = 200

s2

i

0…0 200

R1

s1

R2 = .2 M[R1+2]

R3 = R2 + 1

M[R1] = .2 R3

R2 = M[R1+4]

M[R1+2] = .2 R2

M[R1+2] = M[R1+4]

M[R1+2] = R2

// 2 errors

  • load and store cannot be executed in the same instruction.
  • src and dst addresses cannot be encoded in 4B.

//error, 4B updated

R2

s2 is last declared, lowest address

C++语言程序设计

6 of 38

int a[4];

int i;

for (i=0; i < 4; i++)

a[i] = 0;

i--;

i

R1

a

a[0]

a[1]

a[2]

a[3]

M[R1] = 0

R2 = M[R1]

R3 = M[R1]

R4 = R3 * 4

R5 = R1 + 4

R6 = R4 + R5

M[R6] = 0

//Branch Greater or Equal

// offset

// base

特殊的register,Program Counter,存储当前指令的地址

R2 = M[R1]

R2 = R2 + 1

M[R1] = R2

JMP PC - 40 //10 instructions

R2 = M[R1]

R2 = R2 - 1

M[R1] = R2

BGE R2, 4, PC+?

BGE R2, 4, PC+40

C++语言程序设计

7 of 38

struct fraction

{ int nom;

int denom;

};

fraction p;

p.nom = 22;

p.denom = 7;

((fraction*)&(p.denom))->denom = 4;

nom

R1

7

22

denom

p

M[R1] = 22

M[R1+4] = 7

M[R1+8] = 4

In C, casting doesn’t produce any assembly code.

4

C++语言程序设计

8 of 38

活动记录 activation record

params

形参

saved PC

locals

局部变量

The caller will allocate and initialize

return address after function call

The callee will allocate and initialize

stack segment stores the a.r.

C++语言程序设计

9 of 38

void fun(int b, int *z)

{

char s[4];

short *w;

w = (short*)*(s + 2);

*w = 50;

}

int main(int argc, char** argv)

{

int i = 4;

fun(i, &i);

return 0;

}

saved PC

z

b

s[0…3]

w

fun’s a.r.

C++语言程序设计

10 of 38

int main(int argc, char** argv)

{

int i = 4;

fun(i, &i);

return 0;

}

2

saved PC

argv

argc

SP

Stack Pointer

i

SP = SP – 4 //局部变量i入栈(分配空间)

M[SP] = 4 //为i赋值

SP = SP – 8 //形参入栈

R1 = M[SP+8] //计算第1个形参的值

R2 = SP + 8 //计算第2个形参的值

M[SP] = R1 //给第1个形参赋值

M[SP+4] = R2 //给第2个形参赋值

CALL <fun> //调用函数

SP = SP + 8 //形参出栈

void fun(int b, int *z)

{

char s[4];

short *w;

w = (short*)(s + 2);

*w = 50;

}

saved PC

s[0…3]

w

z

b

main的AR

fun的AR

<fun>:

SP = SP – 8 //局部变量入栈

R1 = SP + 6 //计算地址

M[SP] = R1 //结果写入w的内存

R1 = M[SP] //获取w里存储的地址

M[R1] = .2 50 //往该地址开始的2个字节写入50

SP = SP + 8 //局部变量出栈

RET //将saved PC出栈SP=SP-4,其值放入PC中

SP = SP + 4 //局部变量i出栈

RV = 0 //专门传递返回值的寄存器

RET

执行CALL指令时,把下一条指令的地址,即PC+4压栈,作为saved PC

4

4

50

C++语言程序设计

11 of 38

int fact(int n)

{

if (n == 1)

return 1;

return n*fact(n-1);

}

<fact>:

R1 = M[SP+4]

RV = 1

RET

R1 = M[SP+4]

R1 = R1 – 1

SP = SP – 4

M[SP] = R1

CALL <fact>

SP = SP + 4

R1 = M[SP+4]

RV = RV * R1

RET

saved PC

n

BNE R1, 1, PC+??

BNE R1, 1, PC+12

4

saved PC

3

saved PC

2

saved PC

1

RV

1

2

6

24

C++语言程序设计

12 of 38

void fun()

{

int x;

int y;

x = 11;

y = 17;

swap(&x, &y);

}

void swap(int *a, int *b)

{

int t = *a;

*a = *b;

*b = t;

}

saved PC

x

y

saved PC

b

a

t

SP = SP – 8

M[SP+4] = 11

M[SP] = 17

R1 = SP

R2 = SP + 4

SP= SP – 8

M[SP] = R2

M[Sp+4] = R1

CALL <swap>

SP = SP + 8

SP = SP +8

RET

SP = SP – 4

R1 = M[SP+8]

R2 = M[R1]

M[SP] = R2

<swap>:

R1 = M[SP+12]

R2 = M[R1]

R3 = M[SP+8]

M[R3] = R2

R1 = M[SP]

R2 = M[SP+12]

M[R2] = R1

SP = SP + 4

RET

C++语言程序设计

13 of 38

void swap(int &a, int &b)

{

int t = a;

a = b;

b = t;

}

saved PC

b

a

t

仍是指针

a, b看似standalone integers的别名,但是内部看来a和b是存的指针(地址)

int x;

int y;

x = 11;

y = 17;

swap(x, y);

  • 虽没写&,但并不意味着编译器不用地址,编译器仍使用地址。
  • 引用版swap函数的汇编代码和之前的指针版本的一模一样。
  • x和y成为左值。On Your Behalf
  • 返回一个引用,实际上在背后返回的是一个指针。
  • 引用的工作模式就像是一个会自动解引用的指针

C++语言程序设计

14 of 38

int x = 17;

int y = x;

int &z = y;

int *p = &y;

引用更方便

  • 引用给你一种“错觉”,作为原始变量的别名。
  • 引用无法重新绑定到一个新的左值上,而指针可以。
  • 所以,构造链表无法使用引用。

x

y

z

p

C++语言程序设计

15 of 38

class B

{public:

int fun1(int x, int y);

char* fun2(int *z)

{ int w = *z;

return s + fun1(a, a);}

private:

int a;

char *p;

char s[8];

};

int n = 17;

B b;

b.fun2(&n);

saved PC

w

b

n

z

this

实际上有2个参数。汇编级的调用B::fun2(&b, &n)

成员函数k个参数,实际上有k+1个,多一个this指针

static成员函数和普通函数一样,就像用类名包起来命名空间里的函数。可用函数指针向外提供。

char* fun2(int *z, B &d)

{ int w = *z;

return s + d.fun1(a, a);}

把d的地址加入fun1的AR

saved PC

y

x

fun2

fun1

this

C++语言程序设计

16 of 38

预处理

preprocess

#define

#include

编译

compile

🡪 .o文件

链接

linking

将.o文件stack组成可执行文件

#define WIDTH 40

#define HEIGHT 80

printf(“width is %d”, WIDTH);

int area = WIDTH * HEIGHT;

  • preprocesser只做替换,不管是什么东西。
  • 处理完后的输出仍是文本,交给下一个阶段(编译)。

C++语言程序设计

17 of 38

#define WIDTH 40

#define HEIGHT 80

#define PERIMETER 2*(WIDTH+HEIGHT)

替换为2*(40+80)

不会求值

不知道是整数

#define˽MAX(a,b)˽(((a)>(b))?(a):(b))

注意有这2个空格

MAX(1, 4) preprocessor会替换为 (((1)>(4))?(1):(4))

MAX(1.2, “Hello”) preprocessor不报错只做替换,compiler报错

int m = MAX(fib(100), fact(4000));

C++语言程序设计

18 of 38

#define NthElemAddr(base,eleSize,n) ((char*)base+n*elemSize)

void* StackNth(stack *s, int n)

{

assert(n >= 0);

assert(n < s->logLen);

return NthElemAddr(s->elems, s->elemSize, n);

}

C++语言程序设计

19 of 38

#ifdef NDEBUG

#define assert(cond) (void)0

#else

#define assert(cond) (cond)?((void)0):fprintf(stderr, “…”),exit(0)

#endif

C++语言程序设计

20 of 38

使用宏的问题:

可能会多次计算,如前例MAX展开后:

int m = (((fib(100))>(fact(400)))?(fib(100)):((fact(400));

int larger = MAX(m++, n++);

((m++)>(n++))?(m++):(n++)

大的那个变量会加2次,小的加1次。

gcc –E array.c // 只做预处理,可以看看到底输出什么。

C++语言程序设计

21 of 38

#ifndef …

#define …

#endif

a.h

b.h

c.h

#include “a.h”

#include “b.h”

#include “c.h”

d.c

预处理

编译

……

……

……

……

汇编代码

d.o或d.obj

C++语言程序设计

22 of 38

main.c

#include <stdio.h>

#include <stdlib.h>

#include <assert.h>

int main(int argc, char **argv)0

{

void *m = malloc(400);

assert(m != NULL);

printf(“OK”);

free(m);

return 0;

}

gcc

……

CALL <malloc>

CALL <printf>

CALL <free>

RV = 0

RET

linking

……

……

……

系统库.o中的函数代码

.o文件

.out文件

C++语言程序设计

23 of 38

若注释掉#include <stdio.h>,大多数编译器会报错:未知的函数printf,但gcc不报错。

  • gcc会看到这像一个函数调用,并推断它的原型,报warning:no prototype for printf found,而且会继续编译生成.o文件。
  • gcc会推测返回类型为int(在此例中没问题)。若后面还有printf调用,则它们必须只有一个参数,因为是编译器推导的,与真实的有一点不同。生成的.o文件与之前的一模一样
  • gcc中的LD(Link Load)来做linking。LD去标准库里搜索,看看编译中的warning是否存在对应的函数。printf确实在标准库中,因此在链接阶段会被加进来,即使我们并没有看到过它的原型。

因此,#include并不能保证相应的函数实现在链接时可用。若某个函数在标准库中定义,那么链接时即可加进来,而不论我们是否声明了函数原型。

C++语言程序设计

24 of 38

若注释掉#include <stdlib.h>,

会产生3个warning,生成的.o文件不变。

malloc:1. 未知的函数,推导它的参数是int,返回int。

2. 返回的int赋值给void*变量。

free:未知函数,推导它的参数void*,返回int。

C++语言程序设计

25 of 38

若注释掉#include <assert.h>

会出问题。

编译器认为函数assert的参数是bool类型。

在链接时失败。因为assert在标准库中不存在。

assert是宏,定义在assert.h中。

C++语言程序设计

26 of 38

函数原型( prototype )的存在就是为了让调用者(caller)和被调用者(callee)对saved PC上面的那部分活动记录的布局达成一致,即让实参和形参在数量、类型上一致。

have complete agreement on how everything above the saved PC in the activation record is setup.

C++语言程序设计

27 of 38

函数原型会描述参数的情况,参数被放置在saved PC上面的那部分活动记录。saved PC下面的那部分都是被调函数的事情。

比如当调用printf函数时,会跳转到printf函数的代码。我们必须保证被调函数和主调函数在关于活动记录的上半部分信息是如何叠加的这个问题上保持一致。

C++语言程序设计

28 of 38

int main()

{ int num = 65;

int len = strlen((char*)&num, num);

printf(“length = %d”, len);

return 0;

}

链接时并不报错。链接时gcc只看名称,并不检查参数类型。

运行时,strlen只会看它所需要的activation record。

若要想去掉warning(哪里报warning?),可在前面添加:

int strlen(char*, int); //原型

有时为了不include过多的头文件,手工加上很多原型,节省编译时间,但是写错了原型会有风险。

65

saved PC

65

num

len

strlen

输出结果?

0 或者 1

0 0 0 65

65 0 0 0

big endian

little endian

\0

A \0

C++语言程序设计

29 of 38

int memcmp(void *v1);

……

int main()

{ int n = 17;

int m = memcmp(&n);

}

saved PC

17

n

m

声明的memcmp

v1

实际的memcmp会往上找3个参数

v1

v2

size

memcmp函数需要3个参数,却只给了1个。

int memcmp(void* v1, void* v2, int size);

程序运行时奔溃crash!

C++语言程序设计

30 of 38

  • C语言的编译器会使一些代码可以通过编译。
  • C++需要一切就绪后才行。
  • C只要能用就行。As long as you know what’s going on.

C和C++编译上例函数调用时的差别:

C: CALL <memcpy>

C++: CALL <memcpy_void_p>

所以C++在linking时上例就不会通过,更安全。

为什么C++语言有函数重载,而C语言没有?

C++语言程序设计

31 of 38

segment fault: 解引用了一个非法的指针。

*(NULL) 这个代码不能通过编译,不可对空指针解引用。

但是运行时可能会发生。

BUS error:地址不是合理的。

void* vp = ……;

*(short*)vp = 7;

*(int*)vp = 55;

//50%会出BUS error

如果vp指向4个segment中的一个,不会出segment fault。

硬件和OS会因效率原因做限制:

  • 所有int起始地址须为4的整数倍。
  • short起始地址为偶数。
  • 除short和byte以外,起始地址都是4的整数倍。

stack

heap

code

data

// vp不是4的倍数,则出BUS error

BUS error比segment fault较少出现,一般手工pack data时出现。

C++语言程序设计

32 of 38

int main()

{

int i;

int a[4];

for (i = 0; i <= 4; i++)

a[i] = 0;

return 0;

}

saved PC

0

a[0…3]

i

0

0

0

0

死循环!buffer overflow

换成short a[4];

saved PC

a[0…3]

i

取决于系统是big endian还是little endian

0 0 0 4 正常

4 0 0 0 死循环

C++语言程序设计

33 of 38

void fun()

{

int a[4];

int i;

for (int i = 0; i <= 4; i++)

a[i] = a[i] – 4;

}

saved PC

-4

a[0…3]

i

-4

-4

-4

0~4

CALL <fun>

下一条汇编语句

-4

一直执行fun函数!

C++语言程序设计

34 of 38

int main()

{ DeclareAndInitArray();

PrintArray();

}

void DeclareAndInitArray()

{ int a[100];

int i;

for (i = 0; i < 100; i++)

a[i] = i;

}

void PrintArray()

{ int a[100];

int i;

for (i = 0; i < 100; i++)

printf(“%d\n”, a[i];

}

saved PC

99

a[0…99]

i

1

0

0~99

……

两个函数的AR一样

AR的入栈和出栈只是SP里记录的地址增减,stack里还保留着原来的样子,完成了数组数据的传递!

Channeling技术

C++语言程序设计

35 of 38

int printf(const char* control, …);

printf(“hello”);

printf(“%d+%d=%d”, 4, 4, 8);

成功绑定的占位符的个数,错误返回-1

gcc检查control里占位符的类型与后面的参数类型,并在编译期报告

从一个侧面解释了为什么参数按照从右往左的顺序入栈

4

saved PC

“%d+%d=%d”

SP

4

8

按图索骥

导航地图一样在printf函数的内部指导该如何往上寻找所要的数据。

C++语言程序设计

36 of 38

saved PC

“%d+%d=%d”

SP

无法找到一种可靠且一致的机制,用来在实际代码中找到路线图,并以此按图索骥去理解活动记录中的这部分内容。

???

如果参数按照从左向右的顺序入栈的话:

C++语言程序设计

37 of 38

struct Base

{

int code; // 0

……

};

struct type_one

{

int code; // 1

……

};

struct type_two

{

int code; // 2

……

};

标示类型的code必须第一个声明,这样才会放在结构体内存布局的最低地址,才能用来判断是type_one还是type_two。

C++语言程序设计

38 of 38

stack

heap

code

data

stack

heap

code

data

物理内存

虚拟内存

应用程序1

应用程序2

虚拟内存

内存映射

隔离各个应用程序

造成独占硬件的假象

memory management unit

deamon process

守护进程

C++语言程序设计