How C code is compiled
Compile
C++语言程序设计
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++语言程序设计
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++语言程序设计
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++语言程序设计
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
//error, 4B updated
R2
s2 is last declared, lowest address
C++语言程序设计
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++语言程序设计
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++语言程序设计
活动记录 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++语言程序设计
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++语言程序设计
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++语言程序设计
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++语言程序设计
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++语言程序设计
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);
C++语言程序设计
int x = 17;
int y = x;
int &z = y;
int *p = &y;
引用更方便
x
y
z
p
C++语言程序设计
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++语言程序设计
预处理
preprocess
#define
#include
编译
compile
🡪 .o文件
链接
linking
将.o文件stack组成可执行文件
#define WIDTH 40
#define HEIGHT 80
…
printf(“width is %d”, WIDTH);
int area = WIDTH * HEIGHT;
C++语言程序设计
#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++语言程序设计
#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++语言程序设计
#ifdef NDEBUG
#define assert(cond) (void)0
#else
#define assert(cond) (cond)?((void)0):fprintf(stderr, “…”),exit(0)
#endif
C++语言程序设计
使用宏的问题:
可能会多次计算,如前例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++语言程序设计
#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++语言程序设计
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++语言程序设计
若注释掉#include <stdio.h>,大多数编译器会报错:未知的函数printf,但gcc不报错。
因此,#include并不能保证相应的函数实现在链接时可用。若某个函数在标准库中定义,那么链接时即可加进来,而不论我们是否声明了函数原型。
C++语言程序设计
若注释掉#include <stdlib.h>,
会产生3个warning,生成的.o文件不变。
malloc:1. 未知的函数,推导它的参数是int,返回int。
2. 返回的int赋值给void*变量。
free:未知函数,推导它的参数void*,返回int。
C++语言程序设计
若注释掉#include <assert.h>
会出问题。
编译器认为函数assert的参数是bool类型。
在链接时失败。因为assert在标准库中不存在。
assert是宏,定义在assert.h中。
C++语言程序设计
函数原型( prototype )的存在就是为了让调用者(caller)和被调用者(callee)对saved PC上面的那部分活动记录的布局达成一致,即让实参和形参在数量、类型上一致。
have complete agreement on how everything above the saved PC in the activation record is setup.
C++语言程序设计
函数原型会描述参数的情况,参数被放置在saved PC上面的那部分活动记录。saved PC下面的那部分都是被调函数的事情。
比如当调用printf函数时,会跳转到printf函数的代码。我们必须保证被调函数和主调函数在关于活动记录的上半部分信息是如何叠加的这个问题上保持一致。
C++语言程序设计
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++语言程序设计
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++语言程序设计
C和C++编译上例函数调用时的差别:
C: CALL <memcpy>
C++: CALL <memcpy_void_p>
所以C++在linking时上例就不会通过,更安全。
为什么C++语言有函数重载,而C语言没有?
C++语言程序设计
segment fault: 解引用了一个非法的指针。
*(NULL) 这个代码不能通过编译,不可对空指针解引用。
但是运行时可能会发生。
BUS error:地址不是合理的。
void* vp = ……;
*(short*)vp = 7;
*(int*)vp = 55;
//50%会出BUS error
如果vp指向4个segment中的一个,不会出segment fault。
硬件和OS会因效率原因做限制:
stack
heap
code
data
// vp不是4的倍数,则出BUS error
BUS error比segment fault较少出现,一般手工pack data时出现。
C++语言程序设计
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++语言程序设计
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++语言程序设计
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++语言程序设计
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++语言程序设计
saved PC
“%d+%d=%d”
SP
无法找到一种可靠且一致的机制,用来在实际代码中找到路线图,并以此按图索骥去理解活动记录中的这部分内容。
???
如果参数按照从左向右的顺序入栈的话:
C++语言程序设计
struct Base
{
int code; // 0
……
};
struct type_one
{
int code; // 1
……
};
struct type_two
{
int code; // 2
……
};
标示类型的code必须第一个声明,这样才会放在结构体内存布局的最低地址,才能用来判断是type_one还是type_two。
C++语言程序设计
stack
heap
code
data
stack
heap
code
data
物理内存
虚拟内存
应用程序1
应用程序2
虚拟内存
内存映射
隔离各个应用程序
造成独占硬件的假象
memory management unit
deamon process
守护进程
C++语言程序设计