C语言
推荐教材:
@book{kernighan2006c,
title={The C programming language},
author={Kernighan, Brian W and Ritchie, Dennis M},
year={2006}}
前言
为了充实我的博客内容,C语言相关话题是绕不过去的。从Hello World开始似乎会有些枯燥无聊,但又几乎不会有人自称“精通”C语言,因为高阶问题依然有复杂性。我计划以C语言为主线,穿插GCC、LLVM等相关知识。
Hello World!
1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    int i = 0;
    printf("Hello, World! \n");
    return 0;
}
cc hello.c
./a.out
Hello, World!
a.out 分析
Mach-O格式分析
Mach-O格式分析
推荐使用的工具和文档
  1. ImHex https://github.com/WerWolv/ImHex
  2. otool -h ./a.out
  3. brew install --cask machoview
  4. Hopper Disassembler
  5. XNU源码:https://github.com/apple-oss-distributions/xnu/blob/8d741a5de7ff4191bf97d57b9f54c2f6d4a15585/osfmk/mach/machine.h
header部分,共32个字节
0-3:Magic。0xFEEDFACE是32位,0xFEEDFACF是64位。
4-7:CpuType。0xC是ARM,0x0100000C是ARM64。
8-10:SubCPUType。只有3个字节。0x0,表示CPU_SUBTYPE_ARM_ALL
11:Capability: 1个字节。Capability bits used in the definition of cpu_subtype. 目前只有当表示为Lib64时有值。
12-15:FileType: 4个字节。2表示是可执行文件。1是不可执行的二进制文件,比如静态链接库。还有其它类型,不一一列举。
16-19:NumberOfCommands: 4个字节。表示后面出现的loadCommands的数量。这里是17个指令。
20-23:: SizeOfCommands: 4个字节。表示后面出现的loadCommands的总大小。这里是1032字节。
24-27:Flags: 4个字节。表示一些标志位,每一个bit都有相应的含义。比如是否是动态链接,是否是可执行文件等等。完整的内容如下(太长了,AI辅助生成。)
1. noUndefs:没有未定义的符号。
2. incrLink: 增量链接。
3. dyldLink: 动态链接。
4. bindAtLoad: 加载时绑定。
5. prebind: 预绑定。
6. splitSegAtLoad: 加载时分割。
7. lazyInit: 懒惰初始化。
8. twoLevel: 两级命名空间。
1. forceFlat: 强制扁平化。
2. noMultiDefs: 没有重复定义。
3. noFixPrebinding: 不进行预绑定。
4. prebindable: 可预绑定。
5. allModsBound: 所有模块都已绑定。
6. subSectionViasSymbols: 子节通过符号。
7. canonical: 正规的。
8. weakDefined: 弱定义。
1. bindsToWeak: 绑定到弱符号。
2. allowStackExecution: 允许栈执行。
3. rootSafe: 根安全。
4. setuidSafe: setuid安全。
5. noReexportDylibs: 不重新导出动态库。
6. pie: PIE。
7. deadStrip: 死代码剥离。
8. hasTlvDescriptor: 有TLV描述符。
1. noHeapExecution: 没有堆执行。
2. appExtensionSafe: 应用扩展安全。
3. nlistOutOfSyncWithDyldInfo: nlist与dylidInfo不同步。
4. simSupports: 模拟器支持。
28-31: 未定义
loadCommands部分
这一部分按照[command - commandSize - data]格式构成一个数组,元素个数是上面提到的NumberOfCommands,总长度是上面提到的SizeOfCommands。
指令0:LC_SEGMENT64(__PAGEZERO),vmAddress:0,vmSize:4GB,fileOffset:0,fileSize:0。__PAGEZERO 的作用是在可执行文件的起始地址创建一个不可访问的内存页,以防止程序意外访问无效的内存地址,如空指针解引用,从而提高程序的安全性。
指令1: LC_SEGMENT64(__TEXT),vmAddress: 4GB, vmSize: 16KB,fileOffset: 0, fileSize: 16kB,最大保护:可读可执行,初始保护:可读可执行,4个Section。__TEXT 段在 Mach-O 文件格式中用于存储程序的代码和只读数据,并通过内存保护机制确保其安全性,同时优化了内存使用和程序性能。
Section 0: __text,地址:4294983508(4G + 16212),size:56个字节,偏移:16212(0x3F54),4字节对齐(section alignment (power of 2)),flags: 0x80000400。flags很琐碎,后续补充。__text专门用于存储程序的可执行代码
Section 1: __stubs,地址:4294983564(4G + 16212 + 56),size:12个字节,偏移:16268(0x3F8C),4字节对齐,flags: 0x80000408。__stubs用于存储函数调用的跳转指令,这些指令指向动态链接库中的实际函数实现。
Section 2: __cstring,地址:4294983576(4G + 16212 + 56 + 12),size:16个字节,偏移:16280(0x3F98),1字节对齐,flags: 0x02。__cstring提供了一个集中存储字符串常量的地方,这些字符串常量在程序运行时被引用。"Hello, World!"就在里面
Section 3: __unwind_info,地址:4294983592(4G + 16212 + 56 + 12 + 16),size:88个字节,偏移:16296(0x3FA8),4字节对齐,flags: 0x0。__unwind_info存储了用于异常处理的元数据,这些数据帮助运行时系统在发生异常时正确地展开堆栈,恢复程序的执行环境。
指令2: LC_SEGMENT64(__DATA_CONST),vmAddress: 4294983680(4G + 16212 + 56 + 12 + 16 + 88), vmSize: 16KB,fileOffset: 16kB, fileSize: 16kB,最大保护:可读可写,初始保护:可读可写,1个Section。__DATA_CONST用于存储程序中不会被修改的常量数据。
Section 0: __got,地址:4294983680(4G + 16212 + 56 + 12 + 16 + 88),size:8个字节,偏移:16384(0x4000),8字节对齐,flags: 0x06。__got(Global Offset Table,全局偏移表)节的作用是存储指向动态链接库中符号的指针。当程序运行时,动态链接器会解析这些符号并将其地址写入__got节中。这样,程序在运行时可以通过__got节快速访问动态链接库中的函数和变量,而无需每次都进行符号解析。
指令3: LC_SEGMENT64(__LINKEDIT),vmAddress: 4295000064(4G + 16212 + 56 + 12 + 16 + 88 + 16384), vmSize: 16KB,fileOffset: 32kB, fileSize: 664字节,最大保护:只读,初始保护:只读,0个Section。__LINKEDIT段用于存储链接编辑器所需的信息,包括符号表、重定位条目和字符串表等。
指令3: LC_DYLD_CHAINED_FIXUPS,vmAddress: 4295000064(4G + 16212 + 56 + 12 + 16 + 88 + 16384), vmSize: 16KB,fileOffset: 32kB, fileSize: 664字节,最大保护:只读,初始保护:只读,0个Section。__LINKEDIT段用于存储链接编辑器所需的信息,包括符号表、重定位条目和字符串表等。
0x3F54内容分析
__TEXT里面包含的是可执行代码,在这里就是main函数入口。这里我们拿出反编译工具Hopper Disassembler
汇编代码很简单,所以我就交给AI来给大家翻译了
这段代码是使用ARM64汇编语言编写的,它实现了一个非常简单的功能:打印"Hello, World!"消息。下面是对这段代码的逐行解释:
_main:
0000000100003f54         sub        sp, sp, #0x20
sub sp, sp, #0x20:将栈指针sp向下移动32字节(0x20),为局部变量和保存的寄存器分配空间。
0000000100003f58         stp        fp, lr, [sp, #0x10]
stp fp, lr, [sp, #0x10]:将帧指针fp和链接寄存器lr保存到栈上,位置是栈指针sp加上16字节。
0000000100003f5c         add        fp, sp, #0x10
add fp, sp, #0x10:将帧指针fp设置为当前栈指针sp加上16字节,用于访问函数的局部变量。
0000000100003f60         mov        w8, #0x0
mov w8, #0x0:将寄存器w8(即x8的低位32位)设置为0。
0000000100003f64         str        w8, [sp, #0x10 + var_C]
str w8, [sp, #0x10 + var_C]:将寄存器w8的值存储到栈上,位置是sp加上16字节再减去局部变量var_C的偏移量。
0000000100003f68         stur       wzr, [fp, var_4]
stur wzr, [fp, var_4]:将零寄存器wzr的值存储到帧指针fp减去局部变量var_4的偏移量的位置。
0000000100003f6c         str        wzr, [sp, #0x10 + var_8]
str wzr, [sp, #0x10 + var_8]:将零寄存器wzr的值存储到栈上,位置是sp加上16字节再减去局部变量var_8的偏移量。
0000000100003f70         adrp       x0, #0x100003000                            ; 0x100003f98@PAGE
adrp x0, #0x100003000:将寄存器x0设置为指向页面地址0x100003000的值。
0000000100003f74         add        x0, x0, #0xf98                              ; 0x100003f98@PAGEOFF, argument "format" for method imp___stubs__printf, "Hello, World! \\n"
add x0, x0, #0xf98:将寄存器x0的值加上0xf98,得到完整的地址0x100003f98,这个地址存储了字符串"Hello, World! \n"。
0000000100003f78         bl         imp___stubs__printf                         ; printf
bl imp___stubs__printf:调用printf函数,打印x0寄存器指向的字符串。
0000000100003f7c         ldr        w0, [sp, #0x10 + var_C]
ldr w0, [sp, #0x10 + var_C]:从栈上加载之前存储的值到寄存器w0。
0000000100003f80         ldp        fp, lr, [sp, #0x10]
ldp fp, lr, [sp, #0x10]:从栈上恢复帧指针fp和链接寄存器lr的值。
0000000100003f84         add        sp, sp, #0x20
add sp, sp, #0x20:将栈指针sp向上移动32字节,恢复栈状态。
0000000100003f88         ret
ret:返回到调用函数的地方。
                        ; endp
; endp:表示函数的结束。
总的来说,这段代码设置了一个栈帧,初始化了一些局部变量,调用printf函数打印"Hello, World!",然后清理栈帧并返回。这是一个典型的汇编语言程序结构,展示了如何进行函数调用和栈管理。
0x3FA8内容分析
0x4000内容分析
0x8000内容分析
Symbolic Constants
#define MAX 255
所谓符号常量就是,在预处理阶段会被直接以字面形式替换掉。
字符输入输出
字符回环显示
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
    int c;
    c = getchar();
    while (c != EOF) {
        putchar(c);
        c = getchar();
    }
    return 0;
}
数组
字符计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main () {
    int c, i, nwhite, nother;
    int ndigit[10];
    nwhite = nother = 0;
    for (i = 0; i < 10; ++i)
        ndigit[i] = 0;
    while ((c = getchar()) != EOF)
        if (c >= '0' && c <= '9')
            ++ndigit[c - '0'];
        else if (c == ' ' || c == '\n' || c == '\t')
            ++nwhite;
        else
            ++nother;
    printf("digits =");
    for (i = 0; i < 10; ++i)
        printf(" %d", ndigit[i]);
    printf(", white space = %d, other = %d\n", nwhite, nother);
}
函数
幂函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int power(int, int);

int main() {
    int i;
    for (i = 0; i < 10; i++)
        printf("%d %d %d\n", i, power(2, i), power(-3, i));
    return 0;
}

int power(int base, int n) {
    int i, p;
    p = 1;
    for (i = 1; i <= n; i++)
        p = p * base;
    return p;
}
字符数组
寻找最长行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#define MAX_LINE 1000

int getline2(char line[], int maxline);
void copy(char to[], char from[]);

int main() {
    int len;
    int max;
    char line[MAX_LINE];
    char longest[MAX_LINE];
    max = 0;
    while ((len = getline2(line, MAX_LINE)) > 0)
        if (len > max) {
            max = len;
            copy(longest, line);
        }
    if (max > 0)
        printf("%s", longest);
    return 0;
}

int getline2(char s[], int lim) {
    int c, i;
    for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
        s[i] = c;
    if (c == '\n') {
        s[i] = c;
        ++i;
    }
    s[i] = '\0';
    return i;
}

void copy(char to[], char from[]) {
    int i;
    i = 0;
    while ((to[i] = from[i]) != '\0')
        ++i;
}
# extern关键词
  1. An external variable must be defined, exactly once, outside of any function; this sets aside storage for it.
  2. The variable must also be declared in each function that wants to access it; this states the type of the variable.
  3. The declaration may be an explicit extern statement or may be implicit from context.
  4. In fact, common practice is to place definitions of all external variables at the beginning of the source file, and then omit all extern declarations.
  5. If the program is in several source files, and a variable is defined in file1 and used in file2 and file3, then extern declarations are needed in file2 and file3 to connect the occurrences of the variable.
总结:1处定义,处处声明。可能显式,可能隐式。
那是我日夜思念 深深愛著的人啊