UIUC CS241 讲义:众包系统编程书(2)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: UIUC CS241 讲义:众包系统编程书(2)

UIUC CS241 讲义:众包系统编程书(1)https://developer.aliyun.com/article/1427159

printf调用 write 还是 write 调用printf

printf调用writeprintf包括一个内部缓冲区,所以为了提高性能,printf可能不会在每次调用printf时都调用writeprintf是一个 C 库函数。write是一个系统调用,我们知道系统调用是昂贵的。另一方面,printf使用一个更适合我们需求的缓冲区

如何打印出指针值?整数?字符串?

使用格式说明符“%p”表示指针,“%d”表示整数,“%s”表示字符串。所有格式说明符的完整列表在这里中找到

整数的例子:

int num1 = 10;
printf("%d", num1); //prints num1

整数指针的例子:

int *ptr = (int *) malloc(sizeof(int));
*ptr = 10;
printf("%p\n", ptr); //prints the address pointed to by the pointer
printf("%p\n", &ptr); /*prints the address of pointer -- extremely useful
when dealing with double pointers*/
printf("%d", *ptr); //prints the integer content of ptr
return 0;

字符串的例子:

char *str = (char *) malloc(256 * sizeof(char));
strcpy(str, "Hello there!");
printf("%p\n", str); // print the address in the heap
printf("%s", str);
return 0;

字符串作为指针和数组@ BU

如何将标准输出保存到文件?

最简单的方法:运行你的程序并使用 shell 重定向,例如

./program > output.txt
#To read the contents of the file,
cat output.txt

更复杂的方法:关闭(1),然后使用 open 重新打开文件描述符。参见cs-education.github.io/sys/#chapter/0/section/3/activity/0

指针和数组有什么区别?举一个你可以用其中一个做而另一个做不到的例子。

char ary[] = "Hello";
char *ptr = "Hello";

例子

数组名指向数组的第一个字节。aryptr都可以打印出来:

char ary[] = "Hello";
char *ptr = "Hello";
// Print out address and contents
printf("%p : %s\n", ary, ary);
printf("%p : %s\n", ptr, ptr);

数组是可变的,所以我们可以改变它的内容(但要小心不要写超出数组末尾的字节)。幸运的是,“World”不会比“Hello”更长

在这种情况下,char 指针ptr指向一些只读内存(静态分配的字符串文字存储的地方),所以我们不能改变这些内容。

strcpy(ary, "World"); // OK
strcpy(ptr, "World"); // NOT OK - Segmentation fault (crashes)

然而,与数组不同的是,我们可以将ptr更改为指向另一块内存,

ptr = "World"; // OK!
ptr = ary; // OK!
ary = (..anything..) ; // WONT COMPILE
// ary is doomed to always refer to the original array.
printf("%p : %s\n", ptr, ptr);
strcpy(ptr, "World"); // OK because now ptr is pointing to mutable memory (the array)

从中可以得出的结论是指针*可以指向任何类型的内存,而 C 数组[]只能指向堆栈上的内存。在更常见的情况下,指针将指向堆内存,这种情况下指针引用的内存是可以修改的。

sizeof()返回字节数。所以使用上面的代码,aryptrsizeof()分别是多少?

sizeof(ary): ary是一个数组。返回整个数组所需的字节数(5 个字符+零字节=6 个字节)sizeof(ptr): 与sizeof(char *)相同。返回指针所需的字节数(例如 32 位或 64 位机器的 4 或 8)

sizeof是一个特殊的运算符。实际上,它是编译程序之前编译器替换的东西,因为所有类型的大小在编译时是已知的。当你有sizeof(char*)时,它会获取你的机器上指针的大小(64 位机器为 8 字节,32 位机器为 4 字节等)。当你尝试sizeof(char[])时,编译器会查看并替换整个数组包含的字节数,因为数组的总大小在编译时是已知的。

char str1[] = "will be 11";
char* str2 = "will be 8";
sizeof(str1) //11 because it is an array
sizeof(str2) //8 because it is a pointer

小心,使用 sizeof 获取字符串的长度!

以下代码中哪些是不正确的或正确的,为什么?

int* f1(int *p) {
    *p = 42;
    return p;
} // This code is correct;
char* f2() {
    char p[] = "Hello";
    return p;
} // Incorrect!

解释:在堆栈上为包含 H,e,l,l,o 和一个空字节即(6)字节的正确大小创建了一个数组 p。这个数组存储在堆栈上,在我们从 f2 返回后就无效了。

char* f3() {
    char *p = "Hello";
    return p;
} // OK

解释:p 是一个指针。它保存了字符串常量的地址。字符串常量在 f3 返回后仍然保持不变和有效。

char* f4() {
    static char p[] = "Hello";
    return p;
} // OK

解释:数组是静态的,这意味着它存在于进程的整个生命周期(静态变量不在堆或栈上)。

如何查找 C 库调用和系统调用的信息?

使用 man 手册。请注意,man 手册分为几个部分。第二部分=系统调用。第三部分=C 库。网络:谷歌“man7 open” shell:man -S2 open 或 man -S3 printf

如何在堆上分配内存?

使用 malloc。还有 realloc 和 calloc。通常与 sizeof 一起使用。例如,足够的空间来容纳 10 个整数

int *space = malloc(sizeof(int) * 10);

这个字符串复制代码有什么问题?

void mystrcpy(char*dest, char* src) { 
  // void means no return value 
  while( *src ) { dest = src; src ++; dest++; }  
}

在上面的代码中,它只是改变了 dest 指针指向源字符串。而且 nuls 字节没有被复制。这是一个更好的版本 -

while( *src ) { *dest = *src; src ++; dest++; } 
  *dest = *src;

请注意,通常还会看到以下类型的实现,其中包括在表达式测试中执行所有操作,包括复制 nul 字节。

while( (*dest++ = *src++ )) {};

如何编写一个 strdup 替代品?

// Use strlen+1 to find the zero byte... 
char* mystrdup(char*source) {
   char *p = (char *) malloc ( strlen(source)+1 );
   strcpy(p,source);
   return p;
}

如何在堆上取消分配内存?

使用 free!

int *n = (int *) malloc(sizeof(int));
*n = 10;
//Do some work
free(n);

什么是双重释放错误?如何避免?什么是悬空指针?如何避免?

双重释放错误是当您意外地尝试两次释放相同的分配时发生的。

int *p = malloc(sizeof(int));
free(p);
*p = 123; // Oops! - Dangling pointer! Writing to memory we don't own anymore
free(p); // Oops! - Double free!

修复首先是编写正确的程序!其次,一旦内存被释放,重置指针是良好的编程习惯。这确保了指针在没有程序崩溃的情况下不能被错误使用。

修复:

p = NULL; // Now you can't use this pointer by mistake

缓冲区溢出的一个例子是什么?

著名的例子:心脏出血(将一个 memcpy 复制到一个不足大小的缓冲区)。简单的例子:实现一个 strcpy 并忘记在确定所需内存大小时添加一个 strlen。

“typedef”是什么,你如何使用它?

声明类型的别名。通常与结构一起使用,以减少必须将“struct”写为类型的一部分的视觉混乱。

typedef float real; 
real gravity = 10;
// Also typedef gives us an abstraction over the underlying type used. 
// For example in the future we only need to change this typedef if we
// wanted our physics library to use doubles instead of floats.
typedef struct link link_t; 
//With structs, include the keyword 'struct' as part of the original types

在这个课程中,我们经常使用 typedef 函数。例如,函数的 typedef 可以是这样的

typedef int (*comparator)(void*,void*);
int greater_than(void* a, void* b){
    return a > b;
}
comparator gt = greater_than;

这声明了一个接受两个void*参数并返回整数的比较器函数类型。

哇,这是很多 C 的内容

别担心,还有更多要来的!

下一步:C 编程,第二部分:文本输入和输出

C 编程,第二部分:文本输入和输出

打印到流

如何将字符串、整数、字符打印到标准输出流中?

使用 printf。第一个参数是格式字符串,其中包括要打印的数据的占位符。常见的格式说明符是 %s 将参数视为 C 字符串指针,一直打印到达到 NULL 字符为止;%d 将参数打印为整数;%p 将参数打印为内存地址。

下面显示了一个简单的示例:

char *name = ... ; int score = ...;
printf("Hello %s, your result is %d\n", name, score);
printf("Debug: The string and int are stored at: %p and %p\n", name, &score );
// name already is a char pointer and points to the start of the array. 
// We need "&" to get the address of the int variable

默认情况下,为了性能,printf 实际上并不会写任何东西(通过调用 write),直到它的缓冲区满或打印换行符。

我还可以如何打印字符串和单个字符?

使用 puts( name );putchar( c ),其中 name 是指向 C 字符串的指针,c 只是一个 char

如何将内容打印到其他文件流中?

使用 fprintf( _file_ , "Hello %s, score: %d", name, score); 其中 file 是预定义的 ‘stdout’ ‘stderr’ 或者是由 fopenfdopen 返回的 FILE 指针

我可以使用文件描述符吗?

是的!只需使用 dprintf(int fd, char* format_string, ...); 只需记住流可能是缓冲的,所以您需要确保数据被写入文件描述符。

如何将数据打印到 C 字符串中?

使用 sprintf 或更好的 snprintf

char result[200];
int len = snprintf(result, sizeof(result), "%s:%d", name, score);

snprintf 返回写入的字符数,不包括终止字节。在上面的示例中,这将是最多 199 个。

如果我真的非常想要 printf 调用 write 而不换行怎么办?

使用 fflush( FILE* inp )。文件的内容将被写入。如果我想要写入 “Hello World” 而不换行,我可以这样写。

int main(){
    fprintf(stdout, "Hello World");
    fflush(stdout);
    return 0;
}

perror 有什么帮助?

假设您有一个函数调用刚刚失败了(因为您检查了 man 页面并且它是一个失败的返回代码)。perror(const char* message) 将把错误的英文版本打印到 stderr

int main(){
    int ret = open("IDoNotExist.txt", O_RDONLY);
    if(ret < 0){
        perror("Opening IDoNotExist:");
    }
    //...
    return 0;
}

解析输入

如何从字符串中解析数字?

使用 long int strtol(const char *nptr, char **endptr, int base);long long int strtoll(const char *nptr, char **endptr, int base);

这些函数的作用是获取指向您的字符串 *nptr 和一个 base(即二进制、八进制、十进制、十六进制等)以及一个可选的指针 endptr,并返回解析的整数。

int main(){
    const char *num = "1A2436";
    char* endptr;
    long int parsed = strtol(num, &endptr, 16);
    return 0;
}

但要小心!错误处理有点棘手,因为该函数不会返回错误代码。出错时,它将返回 0,您必须手动检查 errno,但这可能会导致麻烦。

int main(){
    const char *zero = "0";
    char* endptr;
    printf("Parsing number"); //printf sets errno
    long int parsed = strtol(num, &endptr, 16);
    if(parsed == 0){
        perror("Error: "); //oops strtol actually worked!
    }
    return 0;
}

如何使用 scanf 解析输入为参数?

使用 scanf(或 fscanfsscanf)从默认输入流、任意文件流或 C 字符串中获取输入。检查返回值以查看解析了多少项是个好主意。scanf 函数需要有效的指针。将错误的指针值传入是一个常见的错误来源。例如,

int *data = (int *) malloc(sizeof(int));
char *line = "v 10";
char type;
// Good practice: Check scanf parsed the line and read two values:
int ok = 2 == sscanf(line, "%c %d", &type, &data); // pointer error

我们想要将字符值写入 c,将整数值写入 malloc’d 内存。然而我们传递的是数据指针的地址,而不是指针指向的内容!所以 sscanf 将会改变指针本身。也就是说,指针现在将指向地址 10,所以这段代码以后会失败,例如当调用 free(data) 时。

如何阻止 scanf 导致缓冲区溢出?

以下代码假设 scanf 不会读取超过 10 个字符(包括终止字节)到缓冲区中。

char buffer[10];
scanf("%s",buffer);

您可以包含一个可选的整数来指定多少个字符,不包括终止字节:

char buffer[10];
scanf("%9s", buffer); // reads upto 9 charactes from input (leave room for the 10th byte to be the terminating byte)

为什么 gets 是危险的?我应该用什么代替?

以下代码容易受到缓冲区溢出的影响。它假定或信任输入行不会超过 10 个字符,包括终止字节。

char buf[10];
gets(buf); // Remember the array name means the first byte of the array

gets 在 C99 标准中已被弃用,并且已从最新的 C 标准(C11)中删除。程序应该使用 fgetsgetline 代替。

它们分别具有以下结构:

char *fgets (char *str, int num, FILE *stream); 
ssize_t getline(char **lineptr, size_t *n, FILE *stream);

下面是一种简单、安全的读取单行的方法。超过 9 个字符的行将被截断:

char buffer[10];
char *result = fgets(buffer, sizeof(buffer), stdin);

如果出现错误或者到达文件末尾,结果将为 NULL。请注意,与gets不同,fgets会将换行符复制到缓冲区中,您可能希望将其丢弃-

if (!result) { return; /* no data - don't read the buffer contents */}
int i = strlen(buffer) - 1;
if (buffer[i] == '\n') 
    buffer[i] = '\0';

我如何使用getline

getline的优点之一是它将自动(重新)分配足够大小的堆上的缓冲区。

// ssize_t getline(char **lineptr, size_t *n, FILE *stream);
 /* set buffer and size to 0; they will be changed by getline */
char *buffer = NULL;
size_t size = 0;
ssize_t chars = getline(&buffer, &size, stdin);
// Discard newline character if it is present,
if (chars > 0 && buffer[chars-1] == '\n') 
    buffer[chars-1] = '\0';
// Read another line.
// The existing buffer will be re-used, or, if necessary,
// It will be `free`'d and a new larger buffer will `malloc`'d
chars = getline(&buffer, &size, stdin);
// Later... don't forget to free the buffer!
free(buffer);

C 编程,第三部分:常见陷阱

C 程序员常犯哪些常见错误?

内存错误

字符串常量是常量

char array[] = "Hi!"; // array contains a mutable copy 
strcpy(array, "OK");
char *ptr = "Can't change me"; // ptr points to some immutable memory
strcpy(ptr, "Will not work");

字符串文字是存储在程序的代码段中的字符数组,是不可变的。两个字符串文字可能共享内存中的相同空间。以下是一个例子:

char * str1 = "Brandon Chong is the best TA";
char * str2 = "Brandon Chong is the best TA";

str1str2指向的字符串实际上可能驻留在内存中的相同位置。

但是,char 数组包含了从代码段复制到堆栈或静态内存中的文字值。以下 char 数组不驻留在内存中的相同位置。

char arr1[] = "Brandon Chong didn't write this";
char arr2[] = "Brandon Chong didn't write this";

缓冲区溢出/下溢

#define N (10)
int i = N, array[N];
for( ; i >= 0; i--) array[i] = i;

C 语言不检查指针是否有效。上面的例子写入了array[10],这超出了数组边界。这可能会导致内存损坏,因为该内存位置可能正在用于其他用途。实际上,这可能更难发现,因为溢出/下溢可能发生在库调用中。

gets(array); // Let's hope the input is shorter than my array!

返回指向自动变量的指针

int *f() {
    int result = 42;
    static int imok;
    return &imok; // OK - static variables are not on the stack
    return &result; // Not OK
}

自动变量仅绑定到函数的堆栈内存,函数的生命周期结束后继续使用内存是错误的。

内存分配不足

struct User {
   char name[100];
};
typedef struct User user_t;
user_t *user = (user_t *) malloc(sizeof(user));

在上面的例子中,我们需要为结构体分配足够的字节。相反,我们分配了足够的字节来容纳一个指针。一旦我们开始使用用户指针,就会破坏内存。正确的代码如下所示。

struct User {
   char name[100];
};
typedef struct User user_t;
user_t * user = (user_t *) malloc(sizeof(user_t));
字符串需要strlen(s)+1字节

每个字符串在最后一个字符后必须有一个空字节。存储字符串"Hi"需要 3 个字节:[H] [i] [\0]

char *strdup(const char *input) {  /* return a copy of 'input' */
    char *copy;
    copy = malloc(sizeof(char*));     /* nope! this allocates space for a pointer, not a string */
    copy = malloc(strlen(input));     /* Almost...but what about the null terminator? */
    copy = malloc(strlen(input) + 1); /* That's right. */
    strcpy(copy, input);   /* strcpy will provide the null terminator */
    return copy;
}

使用未初始化的变量

int myfunction() {
  int x;
  int y = x + 2;
...

自动变量保存垃圾(内存中发生的任何位模式)。假设它总是初始化为零是错误的。

假设未初始化的内存将被清零

void myfunct() {
   char array[10];
   char *p = malloc(10);

自动(临时变量)不会自动初始化为零。使用 malloc 进行堆分配不会自动初始化为零。

双重释放

char *p = malloc(10);
  free(p);
//  .. later ...
  free(p);

多次释放同一块内存是错误的。

悬空指针

char *p = malloc(10);
  strcpy(p, "Hello");
  free(p);
//  .. later ...
  strcpy(p,"World");

不应使用指向释放内存的指针。一种防御性编程实践是在释放内存后立即将指针设置为 null。

将免费转换为以下片段是一个好主意,它会自动将释放的变量设置为 null:(vim - ultisnips)

snippet free "free(something)" b
free(${1});
$1 = NULL;
${2}
endsnippet

逻辑和程序流错误

忘记 break

int flag = 1; // Will print all three lines.
switch(flag) {
  case 1: printf("I'm printed\n");
  case 2: printf("Me too\n");
  case 3: printf("Me three\n");
}

没有 break 的 case 语句将继续执行下一个 case 语句的代码。正确的代码如下所示。最后一个语句的 break 是不必要的,因为在最后一个语句之后没有更多的要执行的情况。但是,如果添加了更多的情况,可能会导致一些错误。

int flag = 1; // Will print only "I'm printed\n"
switch(flag) {
  case 1: 
    printf("I'm printed\n");
    break;
  case 2: 
    printf("Me too\n");
    break;
  case 3: 
    printf("Me three\n");
    break; //unnecessary
}

等号和相等

int answer = 3; // Will print out the answer.
if (answer = 42) { printf("I've solved the answer! It's %d", answer);}

未声明或错误声明的函数

time_t start = time();

系统函数’time’实际上需要一个参数(一个指向可以接收 time_t 结构的一些内存的指针)。编译器没有捕获到这个错误,因为程序员没有通过包含time.h提供有效的函数原型。

额外的分号

for(int i = 0; i < 5; i++) ; printf("I'm printed once");
while(x < 10); x++ ; // X is never incremented

然而,以下代码是完全可以的。

for(int i = 0; i < 5; i++){
    printf("%d\n", i);;;;;;;;;;;;;
}

这种代码是可以的,因为 C 语言使用分号(;)来分隔语句。如果分号之间没有语句,那么就没有要做的事情,编译器会继续执行下一条语句。

其他陷阱

预处理器

预处理器是什么?它是编译器在实际编译程序之前执行的操作。它是一个复制和粘贴命令。这意味着如果我做以下操作。

#define MAX_LENGTH 10
char buffer[MAX_LENGTH]

预处理后,它会变成这样。

char buffer[10]

C 预处理宏和副作用

#define min(a,b) ((a)<(b) ? (a) : (b))
int x = 4;
if(min(x++, 100)) printf("%d is six", x);

宏是简单的文本替换,因此上面的例子会扩展为x++ < 100 ? x++ : 100(为了清晰起见省略了括号)

C 预处理宏和优先级

#define min(a,b) a<b ? a : b
int x = 99;
int r = 10 + min(99, 100); // r is 100!

宏是简单的文本替换,因此上面的例子会扩展为10 + 99 < 100 ? 99 : 100

C 预处理逻辑陷阱

#define ARRAY_LENGTH(A) (sizeof((A)) / sizeof((A)[0]))
int static_array[10]; // ARRAY_LENGTH(static_array) = 10
int* dynamic_array = malloc(10); // ARRAY_LENGTH(dynamic_array) = 2 or 1

宏有什么问题?如果我们有一个像第一个数组那样的静态数组,它就能工作,因为静态数组的 sizeof 返回数组占用的字节数,将其除以 sizeof(an_element)将给出条目的数量。但是,如果我们使用指向内存块的指针,取指针的 sizeof 并将其除以第一个条目的大小并不总是会给出数组的大小。

sizeof 有什么作用吗?

int a = 0;
size_t size = sizeof(a++);
printf("size: %lu, a: %d", size, a);

代码打印出什么?

size: 4, a: 0

因为 sizeof 实际上不是在运行时评估的。编译器为所有表达式分配类型并丢弃表达式的额外结果。

C 编程,第四部分:字符串和结构

字符串、结构和陷阱

那么什么是字符串?

在 C 中,���们使用空终止字符串,而不是长度前缀,出于历史原因。对于你平常的编程来说,这意味着你需要记住空字符!在 C 中,字符串被定义为一堆字节,直到你达到’\0’或空字节为止。

字符串的两个位置

每当你定义一个常量字符串(即形式为char* str = "constant"的字符串)时,该字符串存储在数据代码段中,这是只读的,这意味着任何尝试修改字符串都会导致段错误。

然而,如果有人malloc空间,就可以更改该字符串为他们想要的任何内容。

内存管理不善

一个常见的陷阱是当你写下面的内容时

char* hello_string = malloc(14);
                       ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
// hello_string ----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e |
                       ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
hello_string = "Hello Bhuvan!";
// (constant string in the text segment)
// hello_string ----> [ "H" , "e" , "l" , "l" , "o" , " " , "B" , "h" , "u" , "v" , "a" , "n" , "!" , "\0" ]
                       ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
// memory_leak -----> | g | a | r | b | a | g | e | g | a | r | b | a | g | e |
                       ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
hello_string[9] = 't'; //segfault!!

我们做了什么?我们为 14 个字节分配了空间,重新分配了指针,成功地导致了段错误!记住跟踪你的指针在做什么。你可能想要做的是使用string.h函数strcpy

strcpy(hello_string, "Hello Bhuvan!");

记住空字节!

忘记对字符串进行空终止会对字符串产生重大影响!边界检查很重要。前面在 wikibook 中提到的 heartbleed 漏洞部分是因为这个原因。

我在哪里可以找到所有这些函数的深入和全面的解释?

就在这里!

字符串信息/比较:strlenstrcmp

int strlen(const char *s) 返回字符串的长度,不包括空字节

int strcmp(const char *s1, const char *s2) 返回一个整数,确定字符串的词典顺序。如果 s1 在字典中出现在 s2 之前,则返回-1。如果两个字符串相等,则返回 0。否则返回 1。

对于大多数这些函数,它们期望字符串是可读的,而不是NULL,但是当你传递NULL时会出现未定义的行为。

字符串修改:strcpystrcatstrdup

char *strcpy(char *dest, const char *src)src的字符串复制到dest假设 dest 有足够的空间容纳 src

char *strcat(char *dest, const char *src)src的字符串连接到目的地的末尾。此函数假定目的地末尾有足够的空间容纳src,包括空字节

char *strdup(const char *dest) 返回字符串的malloc副本。

字符串搜索:strchrstrstr

char *strchr(const char *haystack, int needle) 返回haystackneedle第一次出现的指针。如果找不到,则返回NULL

char *strstr(const char *haystack, const char *needle) 与上面相同,但这次是一个字符串!

字符串标记化:strtok

一个危险但有用的函数strtok接受一个字符串并对其进行标记化。这意味着它将把字符串转换为单独的字符串。这个函数有很多规范,所以请阅读 man 页面,下面是一个人为的例子。

#include <stdio.h>
#include <string.h>
int main(){
    char* upped = strdup("strtok,is,tricky,!!");
    char* start = strtok(upped, ",");
    do{
        printf("%s\n", start);
    }while((start = strtok(NULL, ",")));
    return 0;
}

输出

strtok
is
tricky
!!

当我像这样改变upped时会发生什么?

char* upped = strdup("strtok,is,tricky,,,!!");

内存移动:memcpymemmove

为什么memcpymemmove都在中?因为字符串本质上是带有空字节的原始内存!

void *memcpy(void *dest, const void *src, size_t n) 将从str开始的n个字节移动到dest小心 当内存区域重叠时会出现未定义的行为。这是一个经典的“在我的机器上工作”的例子,因为很多时候 valgrind 无法检测到它,因为在你的机器上它看起来是有效的。当自动评分器出现时,会失败。考虑更安全的版本。

void *memmove(void *dest, const void *src, size_t n) 做与上述相同的事情,但如果内存区域重叠,则保证所有字节都会正确复制过去。

那么struct是什么?

从低级别来看,一个结构体只是一块连续的内存,仅此而已。就像数组一样,结构体有足够的空间来存储所有的成员。但与数组不同,它可以存储不同的类型。考虑上面声明的 contact 结构。

struct contact {
    char firstname[20];
    char lastname[20];
    unsigned int phone;
};
struct contact bhuvan;

简短的插曲

/* a lot of times we will do the following typdef
 so we can just write contact contact1 */
typedef struct contact contact;
contact bhuvan;
/* You can also declare the struct like this to get
 it done in one statement */
typedef struct optional_name {
    ...
} contact;

如果你在没有任何优化和重新排序的情况下编译代码,你可以期望每个变量的地址看起来像这样。

&bhuvan           // 0x100
&bhuvan.firstname // 0x100 = 0x100+0x00
&bhuvan.lastname  // 0x114 = 0x100+0x14
&bhuvan.phone     // 0x128 = 0x100+0x28

因为你的编译器所做的就是说’嘿,保留这么多空间,我会去计算你想要写入的任何变量的偏移量’。

这些偏移量是什么意思?

偏移量是变量开始的地方。电话变量从第0x128字节开始,持续 sizeof(int)字节,但并非总是如此。偏移量并不决定变量的结束位置。考虑在许多内核代码中看到的以下黑客行为。

typedef struct {
    int length;
    char c_str[0];
} string;
const char* to_convert = "bhuvan";
int length = strlen(to_convert);
// Let's convert to a c string
string* bhuvan_name;
bhuvan_name = malloc(sizeof(string) + length+1);
/*
Currently, our memory looks like this with junk in those black spaces
 ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 bhuvan_name = |   |   |   |   |   |   |   |   |   |   |   |
 ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
*/
bhuvan_name->length = length;
/*
This writes the following values to the first four bytes
The rest is still garbage
 ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
 bhuvan_name = | 0 | 0 | 0 | 6 |   |   |   |   |   |   |   |
 ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾
*/
strcpy(bhuvan_name->c_str, to_convert);
/*
Now our string is filled in correctly at the end of the struct
 ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ____
 bhuvan_name = | 0 | 0 | 0 | 6 | b | h | u | v | a | n | \0 |
 ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾‾
*/
strcmp(bhuvan_name->c_str, "bhuvan") == 0 //The strings are equal!

但并不是所有的结构都是完美的

结构体可能需要一些叫做填充(教程)的东西。**我们不指望你在这门课程中对结构体进行打包,只是知道它存在。这是因为在早期(甚至现在)当你必须从内存中获取一个地址时,你必须以 32 位或 64 位块的方式进行。这也意味着你只能请求那些是它的倍数的地址。这意味着

struct picture{
    int height;
    pixel** data;
    int width;
    char* enconding;
}
// You think picture looks like this
           height      data         width     encoding
           ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
picture = |       |               |       |               |
           ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾

概念上可能看起来像这样

struct picture{
    int height;
    char slop1[4];
    pixel** data;
    int width;
    char slop2[4];
    char* enconding;
}
           height   slop1       data        width   slop2  encoding
           ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
picture = |       |       |               |       |       |               |
           ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾

(这是在 64 位系统上)这并不总是这样,因为有时处理器支持不对齐访问。这是什么意思?嗯,有两种选择你可以设置一个属性

struct __attribute__((packed, aligned(4))) picture{
    int height;
    pixel** data;
    int width;
    char* enconding;
}
// Will look like this
           height       data        width     encoding
           ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
picture = |       |               |       |               |
           ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾

但现在每次我想要访问dataencoding,我都必须进行两次内存访问。你可以做的另一件事是重新排列结构,尽管这并不总是可能的

struct picture{
    int height;
    int width;
    pixel** data;
    char* enconding;
}
// You think picture looks like this
           height   width        data         encoding
           ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
picture = |       |       |               |               |
           ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾ ‾‾‾

C 编程,第五部分:调试

《C 程序调试指南》

这将是一个帮助您调试 C 程序的大型指南。您可以检查错误的不同级别,我们将逐个介绍。请随时添加您在调试 C 程序中发现有用的任何内容,包括但不限于,调试器的使用,识别常见错误类型,陷阱和有效的搜索技巧。

在代码中调试

清洁代码

使用辅助函数使您的代码模块化。如果有重复的任务(例如在 MP2 中获取连续块的指针),请将它们制作为辅助函数。确保每个函数都非常擅长做一件事,这样您就不必两次调试。

假设我们正在通过每次迭代找到最小元素来进行选择排序,如下所示,

void selection_sort(int *a, long len){
     for(long i = len-1; i > 0; --i){
         long max_index = i;
         for(long j = len-1; j >= 0; --j){
             if(a[max_index] < a[j]){
                  max_index = j;
             }
         }
         int temp = a[i];
         a[i] = a[max_index];
         a[max_index] = temp;
     }
}

许多人可以看到代码中的错误,但将上述方法重构为

long max_index(int *a, long start, long end);
void swap(int *a, long idx1, long idx2);
void selection_sort(int *a, long len);

而错误特别在一个函数中。

最后,我们不是一个关于重构/调试代码的课程–事实上,大多数系统代码都很糟糕,你不想读它。但是为了调试,长远来看,采用一些实践可能对你有好处。

断言!

使用断言来确保您的代码在某个特定点之前工作–并且重要的是,确保您以后不会破坏它。例如,如果您的数据结构是双向链表,您可以这样做,assert(node->size == node->next->prev->size)来断言下一个节点指向当前节点。您还可以检查指针是否指向预期的内存地址范围,而不是 null,->size 是合理的等等。NDEBUG 宏将禁用所有断言,因此在调试完成后不要忘记设置它。www.cplusplus.com/reference/cassert/assert/

使用 assert 的一个快速示例是,假设我正在使用 memcpy 编写代码

assert(!(src < dest+n && dest < src+n)); //Checks overlap
memcpy(dest, src, n);

这个检查可以在编译时关闭,但会帮助您避免大量的调试麻烦!

printfs

当一切都失败时,疯狂地打印!您的每个函数都应该知道它要做什么(例如,find_min 最好找到最小的元素)。您希望测试每个函数是否正在做它设定的事情,并确切地查看代码在哪里出错。在竞态条件的情况下,tsan 可能有所帮助,但让每个线程在特定时间打印数据可能有助于您识别竞态条件。

Valgrind

(待办事项)

Tsan

ThreadSanitizer 是 Google 的一个工具,内置在 clang(和 gcc)中,可以帮助您检测代码中的竞态条件。有关该工具的更多信息,请参阅 Github 维基。

请注意,使用 tsan 会使您的代码变慢一些。

#include <pthread.h>
#include <stdio.h>
int Global;
void *Thread1(void *x) {
    Global++;
    return NULL;
}
int main() {
    pthread_t t[2];
    pthread_create(&t[0], NULL, Thread1, NULL);
    Global = 100;
    pthread_join(t[0], NULL);
}
// compile with gcc -fsanitize=thread -pie -fPIC -ltsan -g simple_race.c

我们可以看到变量 Global 存在竞态条件。主线程和使用 pthread_create 创建的线程将尝试同时更改值。但是,ThreadSantizer 能否捕捉到它呢?

$ ./a.out
==================
WARNING: ThreadSanitizer: data race (pid=28888)
  Read of size 4 at 0x7f73ed91c078 by thread T1:
    #0 Thread1 /home/zmick2/simple_race.c:7 (exe+0x000000000a50)
    #1  :0 (libtsan.so.0+0x00000001b459)
  Previous write of size 4 at 0x7f73ed91c078 by main thread:
    #0 main /home/zmick2/simple_race.c:14 (exe+0x000000000ac8)
  Thread T1 (tid=28889, running) created by main thread at:
    #0  :0 (libtsan.so.0+0x00000001f6ab)
    #1 main /home/zmick2/simple_race.c:13 (exe+0x000000000ab8)
SUMMARY: ThreadSanitizer: data race /home/zmick2/simple_race.c:7 Thread1
==================
ThreadSanitizer: reported 1 warnings

如果我们使用调试标志编译,那么它将给我们变量名。

GDB

介绍:www.cs.cmu.edu/~gilpin/tutorial/

以编程方式设置断点

在使用 GDB 调试复杂的 C 程序时,一个非常有用的技巧是在源代码中设置断点。

int main() {
    int val = 1;
    val = 42;
    asm("int $3"); // set a breakpoint here
    val = 7;
}
$ gcc main.c -g -o main && ./main
(gdb) r
[...]
Program received signal SIGTRAP, Trace/breakpoint trap.
main () at main.c:6
6       val = 7;
(gdb) p val
$1 = 42
检查内存内容

www.delorie.com/gnu/docs/gdb/gdb_56.html

例如,

int main() {
    char bad_string[3] = {'C', 'a', 't'};
    printf("%s", bad_string);
}
$ gcc main.c -g -o main && ./main
$ Cat ZVQ�� $
(gdb) l
1   #include <stdio.h>
2   int main() {
3       char bad_string[3] = {'C', 'a', 't'};
4       printf("%s", bad_string);
5   }
(gdb) b 4
Breakpoint 1 at 0x100000f57: file main.c, line 4.
(gdb) r
[...]
Breakpoint 1, main () at main.c:4
4       printf("%s", bad_string);
(gdb) x/16xb bad_string
0x7fff5fbff9cd: 0x63    0x61    0x74    0xe0    0xf9    0xbf    0x5f    0xff
0x7fff5fbff9d5: 0x7f    0x00    0x00    0xfd    0xb5    0x23    0x89    0xff
(gdb)

在这里,通过使用带有参数16xbx命令,我们可以看到从内存地址0x7fff5fbff9cbad_string的值)开始,printf 实际上会看到以下字节序列作为字符串,因为我们提供了一个没有空终止符的格式不正确的字符串。

0x43 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7f 0x00

C 编程,复习问题

主题

  • C 字符串表示
  • C 字符串作为指针
  • char p[]vs char* p
  • 简单的 C 字符串函数(strcmp,strcat,strcpy)
  • sizeof char
  • sizeof x vs x*
  • 堆内存寿命
  • 堆分配调用
  • 解引用指针
  • 取地址运算符
  • 指针算术
  • 字符串复制
  • 字符串截断
  • 双重释放错误
  • 字符串字面值
  • 打印格式。
  • 内存越界错误
  • 静态内存
  • fileio POSIX v C 库
  • C io fprintf 和 printf
  • POSIX 文件 io(读|写|打开)
  • stdout 的缓冲

问题/练习

  • 以下打印出什么
int main(){
    fprintf(stderr, "Hello ");
    fprintf(stdout, "It's a small ");
    fprintf(stderr, "World\n");
    fprintf(stdout, "place\n");
    return 0;
}
  • 以下两个声明之间有什么区别?其中一个的sizeof返回什么?
char str1[] = "bhuvan";
char *str2 = "another one";
  • C 中的字符串是什么?
  • 编写一个简单的my_strcmpmy_strcatmy_strcpymy_strdup呢?奖励:只通过字符串一次编写函数。
  • 以下通常应该返回什么?
int *ptr;
sizeof(ptr);
sizeof(*ptr);
  • 什么是malloc?它与calloc有什么不同。一旦内存被malloc,我如何使用realloc
  • &运算符是什么?*呢?
  • 指针算术。假设以下地址。以下移位是什么?
char** ptr = malloc(10); //0x100
ptr[0] = malloc(20); //0x200
ptr[1] = malloc(20); //0x300
* `ptr + 2`
 * `ptr + 4`
 * `ptr[0] + 4`
 * `ptr[1] + 2000`
 * `*((int)(ptr + 1)) + 3`
  • 我们如何防止双重释放错误?
  • 打印字符串,intchar的 printf 格式说明符是什么?
  • 以下代码有效吗?如果是,为什么?output位于哪里?
char *foo(int var){
    static char output[20];
    snprintf(output, 20, "%d", var);
    return output;
}
  • 编写一个接受字符串并打开该文件的函数,每次打印出文件的 40 个字节,但每隔一次打印都会颠倒字符串(尝试使用 POSIX API 实现)。
  • POSIX 文件描述符模型和 C 的FILE*之间有哪些区别(即使用了哪些函数调用,哪个是缓冲的)?POSIX 内部使用 C 的FILE*还是反之亦然?

返回:C 编程,第五部分:调试

二、进程

进程,第一部分:介绍

概述

进程是正在运行的程序(有点)。进程也只是计算机程序运行的一个实例。进程有很多可用的东西。在每个程序开始时,您会得到一个进程,但每个程序都可以创建更多的进程。事实上,您的操作系统只启动一个进程,所有其他进程都是从那个进程分叉出来的——在启动时都是在后台完成的。

在开始时

当您的 Linux 机器上的操作系统启动时,会创建一个名为init.d的进程。该进程是一个特殊的进程,处理信号、中断和某些内核元素的持久性模块。每当您想要创建一个新进程时,都会调用fork(将在后面的部分讨论)并使用另一个函数来加载另一个程序。

进程隔离

进程非常强大,但它们是隔离的!这意味着默认情况下,没有进程可以与另一个进程通信。这非常重要,因为如果您有一个庞大的系统(比如 EWS),那么您希望一些进程具有更高的特权(监控、管理),而您绝对不希望普通用户能够故意或者意外地通过修改进程来使整个系统崩溃。

如果我运行以下代码,

int secrets; //maybe defined in the kernel or else where
secrets++;
printf("%d\n", secrets);

在两个不同的终端上,正如您所猜测的,它们都会打印出 1 而不是 2。即使我们改变代码以执行一些非常巧妙的操作(除了直接读取内存),也没有办法改变另一个进程的状态(好吧,也许这个,但那就有点太深入了)。

进程内容

内存布局

当一个进程启动时,它会得到自己的地址空间。这意味着每个进程都会得到(对于内存

  • 堆栈。堆栈是存储自动变量和函数调用返回地址的地方。每次声明一个新变量,程序都会将堆栈指针向下移动,以保留变量的空间。堆栈的这一部分是可写的,但不可执行。如果堆栈增长得太远(意味着它要么超出了预设的边界,要么与堆相交),您很可能会得到堆栈溢出,最终导致段错误或类似的错误。默认情况下,堆栈是静态分配的,这意味着只有一定数量的空间可以写入
  • 。堆是一个不断扩大的内存区域。如果要分配一个大对象,它就会放在这里。堆从文本段的顶部开始向上增长(这意味着有时当您调用malloc时,它会要求操作系统将堆边界向上推)。这个区域也是可写的,但不可执行。如果系统受限或者地址用完了(在 32 位系统上更常见),就可能用完堆内存。
  • 数据段。这包含了所有的全局变量。这一部分从文本段的末尾开始,大小是静态的,因为全局变量的数量在编译时就已知。这一部分是可写的,但不可执行,没有其他太花哨的东西。
  • 文本段。这可以说是地址中最重要的部分。这是存储所有代码的地方。由于汇编编译成了 1 和 0,这就是 1 和 0 存储的地方。程序计数器在这个段中执行指令,并向下移动到下一个指令。重要的是要注意,这是代码中唯一可执行的部分。如果您尝试在运行时更改代码,很可能会导致段错误(虽然有办法绕过,但假设它会导致段错误)。
  • 为什么它不从零开始?这超出了本课程的范围,但这是出于安全考虑。

文件描述符


正如小册子所示,操作系统跟踪文件描述符及其指向的内容。我们将在后面看到,文件描述符不一定指向实际文件,操作系统会为您跟踪它们。另外,请注意,在进程之间文件描述符可能会被重用,但在进程内部它们是唯一的。

文件描述符也有位置的概念。您可以完全从磁盘上读取文件,因为操作系统跟踪文件中的位置,并且该位置也属于您的进程。

安全/权限

进程功能/限制(奖励)

当您复习期末考试时,您可以回来看看进程也具有所有这些东西。第一次看时 - 它可能不太有意义。

进程 ID(PID)

为了跟踪所有这些进程,您的操作系统为每个进程分配一个数字,该进程称为 PID,即进程 ID。

进程还可以包含

  • 映射
  • 状态
  • 文件描述符
  • 权限

分叉,第一部分:介绍

UIUC CS241 讲义:众包系统编程书(3)https://developer.aliyun.com/article/1427161

相关文章
|
7月前
|
存储 缓存 安全
UIUC CS241 讲义:众包系统编程书(4)
UIUC CS241 讲义:众包系统编程书(4)
219 0
|
7月前
|
存储 缓存 网络协议
UIUC CS241 讲义:众包系统编程书(7)
UIUC CS241 讲义:众包系统编程书(7)
289 0
|
7月前
|
存储 安全 网络协议
UIUC CS241 讲义:众包系统编程书(8)
UIUC CS241 讲义:众包系统编程书(8)
213 0
|
7月前
|
存储 NoSQL 编译器
UIUC CS241 讲义:众包系统编程书(1)
UIUC CS241 讲义:众包系统编程书(1)
88 0
|
16天前
|
人工智能 数据挖掘
AI长脑子了?LLM惊现人类脑叶结构并有数学代码分区,MIT大牛新作震惊学界!
麻省理工学院的一项新研究揭示了大型语言模型(LLM)内部概念空间的几何结构,与人脑类似。研究通过分析稀疏自编码器生成的高维向量,发现了概念空间在原子、大脑和星系三个层次上的独特结构,为理解LLM的内部机制提供了新视角。论文地址:https://arxiv.org/abs/2410.19750
58 12
|
7月前
|
网络协议 算法 安全
UIUC CS241 讲义:众包系统编程书(6)
UIUC CS241 讲义:众包系统编程书(6)
136 0
|
7月前
|
存储 缓存 算法
UIUC CS241 讲义:众包系统编程书(5)
UIUC CS241 讲义:众包系统编程书(5)
217 0
|
7月前
|
安全 Unix 编译器
MIT 6.858 计算机系统安全讲义 2014 秋季(一)(4)
MIT 6.858 计算机系统安全讲义 2014 秋季(一)
75 2
|
7月前
|
传感器 Web App开发 安全
MIT 6.858 计算机系统安全讲义 2014 秋季(四)(2)
MIT 6.858 计算机系统安全讲义 2014 秋季(四)
69 1
|
7月前
|
网络协议 安全 网络安全
MIT 6.858 计算机系统安全讲义 2014 秋季(二)(4)
MIT 6.858 计算机系统安全讲义 2014 秋季(二)
60 0