精华内容
下载资源
问答
  • 在内核中维护者一张符号表,记录了内核中所有的符号(函数、全局变量等)的地址以及名字,这个符号表被嵌入到内核镜像中,使得内核可以在运行过程中随时获得一个符号地址对应的符号名。而内核代码中可以通过  ...

    内核中的dump_stack()

    获得内核中当前进程的栈回溯信息需要用到的最重要的三个内容就是:

    栈指针:sp寄存器,用来跟踪程序执行过程。

    返回地址:ra寄存器,用来获取函数的返回地址。

    程序计数器:epc,用于定位当前指令的位置。

    本文的内容都是基于mips体系架构的,如果你不搞mips,就只看个大致流程就可以了,不然可能会被某些内容误导。在ARM中,这三个寄存器分别为SP、LR和PC寄存器。

    dump_stack()用于回溯函数调用关系,他需要做的工作很简单:

    1.   从进程栈中找到当前函数(callee)的返回地址。

    2.   根据函数返回地址,从代码段中定位该地址位于哪个函数中,找到的函数即为caller函数。

    3.   打印caller函数的函数名。

    4.   重复前3个步骤。直到返回值为0或不在内核记录的符号表范围内。

    在编译程序的时候,所有函数所需要的栈空间的大小都已经计算出来,如果函数需要保存返回地址,返回地址在该函数的栈空间中保存的位置也都计算出来了。所以,我们想得到返回地址,只需得到每个函数栈即可,而所有函数栈都放在进程的栈中,栈顶为sp。

    返回地址是caller函数中将要执行的指令,是指向代码段的,这个更容易得到,因为代码段在编译时就确定了。

    当前函数的位置通过pc的值可以得到。

    例如,现在有func0调用func1,func1又调用func2,在func2执行过程中,进程栈空间大致如下:


    左图为栈空间,栈顶为sp,右图为程序代码的部分内容。右图中的实曲线表示出了函数之间的调用和返回关系。调用关系通过跳转指令完成,返回地址通过左图每个函数栈空间中存储的返回地址指定。这样我们就可以得到函数的调用关系,并通过每个函数的地址打印出函数名。

    那dump_stack的工作流程就很清楚了。我就不帖代码了,因为基本上都是体系结构相关的操作。

    需要说明的一个地方是,通过函数的地址来打印函数名是通过格式控制符%pS来打印的:

    printk("[<%p>] %pS\n", (void *) ip,(void *) ip);

    在内核代码树的lib/vsprintf.c中的pointer函数中,说明了printk中的%pS的意思:

    [cpp]  view plain  copy
    1. case 'S':  
    2.        return symbol_string(buf, end, ptr, spec, *fmt);  

    即'S'表示打印符号名,而这个符号名是kallsyms里获取的。

    可以看一下kernel/kallsyms.c中的kallsyms_lookup()函数,它负责通过地址找到函数名,分为两部分:

    1. 如果地址在编译内核时得到的地址范围内,就查找kallsyms_names数组来获得函数名。

    2. 如果这个地址是某个内核模块中的函数,则在模块加载后的地址表中查找。

    kallsyms_lookup()最终返回字符串“函数名+offset/size[mod]”,交给printk打印。

    关于内核符号表kallsyms_names可参考我的另一篇文章点击打开链接

    实现应用程序中的dump_stack()

    按照如上所述,实现一个用户态程序的dump_stack好像不是什么难事,因为上面说的步骤在用户态都可以完成,程序运行的方式也基本上是相同的。

    那我们实现一个dump_stack需要做的事情只有两点:

    1.   获得程序当前运行时间点的pc值和栈指针sp。这样就可以得到每个函数栈中的返回地址。

    2.   构造和内核符号表相同的应用程序符号表。

    需要注意,不同用户进程都拥有自己的虚拟地址空间,所以栈回溯只能在本进程中完成。

    具体实现当然也是体系结构相关的。既然原理都知道了,那我就直接给出代码供参考(mips的)。代码见https://github.com/castoz/backtrace

    其中backtrace.c实现了栈回溯,uallsyms.c用于生成符号表,main.c中为测试代码。
    backtrace.c中提供了两个接口供其他文件调用:
    show_backtrace():打印函数的回溯信息。
    addr_to_name(addr):打印addr对应的函数名。
    uallsyms.c文件直接使用内核中的scripts/kallsyms.c,只需要做少量修改,具体的改动为:
    1. 符号基准地址改为__start。
    2. 需要记录的符号范围改为在_init到_fini之间或_init到_end之间。
    3. 维护uallsyms_addresses、uallsyms_num_syms和uallsyms_names三个全局变量,不使用压缩算法,所以不需要其他三个全局变量。
    4. 在生成的汇编代码中删除"#include <asm/types.h>"一行,因为在编译时不需要。

    测试文件main.c的内容:

    [cpp]  view plain  copy
    1. #include <stdio.h>  
    2. #include "backtrace.h"  
    3.   
    4. int func2(int a, int b);  
    5. int func1(int a, int b);  
    6. int func0(int a, int b);  
    7.   
    8. int func2(int a, int b)  
    9. {  
    10.     int c = a * b;  
    11.     printf("%s: c = %d\n", __FUNCTION__, c);  
    12.     show_backtrace();  
    13.     return c;  
    14. }  
    15.   
    16. int func1(int a, int b)  
    17. {  
    18.     int c = func2(a, b);  
    19.     printf("%s: c = %d\n", __FUNCTION__, c);  
    20.     return c;  
    21. }  
    22.   
    23. int func0(int a, int b)  
    24. {  
    25.     int c = func1(a, b);  
    26.     printf("%s: c = %d\n", __FUNCTION__, c);  
    27.     return c;  
    28. }  
    29.   
    30. int main()  
    31. {  
    32.     int a = 4, b = 5;  
    33.     int (*funcptr)(intint) = func0;  
    34.       
    35.     int c = func0(a, b);  
    36.     printf("%s: c = %d\n", __FUNCTION__, c);  
    37.       
    38.     printf("funcptr's name = %s\n", addr_to_name((unsigned long)funcptr));  
    39.     return 0;  
    40. }  
    执行make all生成可执行文件testbt,放到mips的系统上运行。
    运行结果:
    [plain]  view plain  copy
    1. root@openwrt:/tmp# ./testbt  
    2. func2: c = 20  
    3. 4362  
    4. Call trace:  
    5.         =>show_backtrace()+0x20  
    6.         =>func2()+0x34  
    7.         =>func1()+0x10  
    8.         =>func0()+0x10  
    9.         =>main()+0x14  
    10.   
    11.   
    12. func1: c = 20  
    13. func0: c = 20  
    14. main: c = 20  
    15. funcptr's name = func0  

    参考自:http://blog.csdn.net/jasonchen_gbd/article/details/44066815

    在内核中维护者一张符号表,记录了内核中所有的符号(函数、全局变量等)的地址以及名字,这个符号表被嵌入到内核镜像中,使得内核可以在运行过程中随时获得一个符号地址对应的符号名。而内核代码中可以通过 printk("%pS\n", addr) 打印符号名。

    本文介绍内核符号表的生成和查找过程。

    1. System.map和/proc/kallsyms

    System.map文件是编译内核时生成的,它记录了内核中的符号列表,以及符号在内存中的虚拟地址。这个文件通过nm命令生成,具体可参考内核目录下的scripts/mksysmap脚本。System.map中每个条目由三部分组成,例如:

    f0081e80 T alloc_vfsmnt

    即“地址  符号类型  符号名”

    其中符号类型有如下几种:

    •   A =Absolute
    •   B =Uninitialised data (.bss)
    •   C = Comonsymbol
    •   D =Initialised data
    •   G =Initialised data for small objects
    •   I = Indirectreference to another symbol
    •   N =Debugging symbol
    •   R = Readonly
    •   S =Uninitialised data for small objects
    •   T = Textcode symbol
    •   U =Undefined symbol
    •   V = Weaksymbol
    •   W = Weaksymbol
    •  Corresponding small letters are local symbols

    /proc/kallsyms文件是在内核启动后生成的,位于文件系统的/proc目录下,实现代码见kernel/kallsyms.c。前提是内核必须打开CONFIG_KALLSYMS编译选项。它和System.map的区别是它同时包含了内核模块的符号列表。

    通常情况下我们只需要_stext~_etext_sinittext~_einittext之间的符号,如果需要将nm命令获得的所有符号都记录下来,则需要开启内核的CONFIG_KALLSYMS_ALL编译选项,不过一般是不需要打开的。

    2. 内核符号表

    内核在执行过程中,可能需要获得一个地址所在的函数,比如在输出某些调试信息的时候。一个典型的例子就是使用dump_stack()函数打印栈回溯信息。

    但是内核在查找一个地址对应的函数名时,没有求助于上述两个文件,而是在编译内核时,向vmlinux嵌入了一个符号表,这样做可能是为了方便快速的查找并避免文件操作带来的不良影响。

    2.1 内核符号表的结构

    内嵌的符号表是通过内核目录下的scripts/kallsyms工具生成的,工具的源码为相同目录下的kallsyms.c。这个工具的用法如下:

    [plain]  view plain  copy
    1. nm -n vmlinux | scripts/kallsyms [--all-symbols] > symbols.S  

    可见同样是通过nm命令得到vmlinux的符号表,并将这些符号表信息进行调整,最终生成一个汇编文件。这个汇编文件中包含了6个全局变量:kallsyms_addresses,kallsyms_num_syms,kallsyms_names,kallsyms_markers,kallsyms_token_tablekallsyms_token_index,其中:

    • kallsyms_addresses:一个数组,存放所有符号的地址列表,按地址升序排列。
    • kallsyms_num_syms:符号的数量。
    • kallsyms_names:一个数组,存放所有符号的名称,和kallsyms_addresses一一对应。

    其他三个全局变量的含义后续会提到。

    这些变量被嵌入在vmlinux中,所以在内核代码中直接extern就可以使用。例如dump_stack()就是通过这些变量来查找一个地址对应的函数名的。

    那由scripts/kallsyms生成的汇编文件是如何嵌入到vmlinux中的呢。在编译内核的后期主要进行了一下几步额外的编译和链接过程:

    1.   链接器ld将内核的绝大部分组件链接成临时内核映像.tmp_vmlinux1。
    2.   使用nm命令将.tmp_vmlinux1中符号和相对的地址导出来,并使用kallsyms工具生成tmp_kallsyms1.S的文件。
    3.   对.tmp_kallsyms1.S文件进行编译生成.tmp_kallsyms1.o文件。
    4.   重复1的链接过程,这次将步骤3得到的.tmp_kallsyms1.o文件链接进入内核得到临时内核映像.tmp_vmlinux2文件,其中包含的部分函数和非栈变量的地址发生了变化,但但由于.tmp_kallsyms1.S中的符号表还是旧的,所以.tmp_vmlinux2还不能作为最终的内核映像。
    5.   再使用nm命令将.tmp_vmlinux2中符号和相对的地址导出来,并使用kallsyms工具生成tmp_kallsyms2.S的文件。
    6.   对.tmp_kallsyms2.S文件进行编译生成.tmp_kallsyms2.o文件。
    7.   .tmp_kallsyms2.o即为最终的kallsyms.o目标,并链接进入内核生成vmlinux文件。

    此时,上面的那6个全局变量被写进vmlinux中的“.rodata”段(所以还是叫全局常量吧),内核代码就可以使用了,使用前需extern一下:

    [cpp]  view plain  copy
    1. extern const unsigned long kallsyms_addresses[] __attribute__((weak));  

    weak属性表示当我们不确定外部模块是否提供了某个变量或函数时,可以将这个变量或函数定义为弱属性,如果外部有定义则使用,没有定义则相当于自己定义。

    在使用这6个全局常量之前,我们先要弄清楚他们都是干什么用的。kallsyms_addresses、kallsyms_num_syms和kallsyms_names在前面已经讲过,实际上他们已经可以提供一个[地址 : 符号]的映射关系了,但是内核中几万个符号这样一条一条的存起来会占用大量的空间,所以内核采用一种压缩算法,将所有符号中出现频率较高的字符串记录成一个个的token,然后将原来的符号中和token匹配的子串进行压缩,这样可以实现使用一个字符来代替n个字符,以减小符号存储长度。

    因此符号表维护了一个kallsyms_token_table,他有256个元素,对应一个字节的长度。由于符号名的只能出现下划线、数字和字母,那在kallsyms_token_table[256]数组中,除了这些字符的ASCII码对应的位置,还有很多未被使用的位置就可以用来存储压缩串。kallsyms_token_table表的内容像下面这样:

    [cpp]  view plain  copy
    1. kallsyms_token_table:  
    2.    .asciz  "end"  
    3.    .asciz  "Tjffs2"  
    4.    .asciz  "map_"  
    5.    .asciz  "int"  
    6.    .asciz  "to_"  
    7.    .asciz  "Tn"  
    8. .asciz  "t__"  
    9. .asciz  "unregist"  
    10. ... ...  
    11. .asciz  "a"  
    12. .asciz  "b"  
    13. .asciz  "c"  
    14. .asciz  "d"  
    15. .asciz  "e"  
    16. .asciz  "f"  
    17. .asciz  "g"  
    18. .asciz  "h"  
    19. ... ...  

    那我们在表示一个函数名时,就可以用0x00来表示“end”,用0x04来表示“to_”等。没有被压缩的如0x61仍然表示“a”。

    kallsyms_token_index记录每个token首字符在kallsyms_token_table中的偏移。同token table共256条,在打印token时需要用到。

    [cpp]  view plain  copy
    1. kallsyms_token_index:  
    2.    .short  0  
    3.    .short  4     //Tjffs2第一个字符在kallsyms_token_table中的偏移  
    4.    .short  11  
    5.    .short  16  

    至于kallsyms_token_table表是如何生成的,可以阅读scripts/kallsyms.c的实现,大致就是将所有符号出现的相邻的两个字符出现的次数都记录起来,例如对于“nf_nat_nf_init”,就记录下“nf”、“f_”、“_n”、“na”、……,每两个字符组合出现的次数记录在token_profit[0x10000]数组中(两个字符一组,共有2^8 * 2^8 = 0x10000中可能组合),然后挑选出现次数最多的一个组合形成一个token,比如用“g”来表示“nf”,那“nf_nat_nf_init”就被改为“g_nat_g_init”。接下来,再在修改后的所有符号中计算每两个字符的出现次数来挑选出现次数最多的组合,例如用“J”来表示“g_”,那“g_nat_g_init”又被改为“Jnat_Jinit”。直到生成最终的token表。

    2.2 内核查找一个符号的过程

    这时还没讲到全局常量kallsyms_markers。我们先来看内核如何根据这六个全局常量来查找一个地址对应的函数名的,实现函数为kernel/kallsyms.c中的kallsyms_lookup()。

    我不讲函数实现,只是用一个例子来说明内核符号的查找过程:

    比如我在内核中想打印出0x80216bf4地址所在的函数。首先不管内核怎么做,我们可以先在System.map文件中看到这个地址位于为nf_register_hook和nf_register_hooks两个符号之间,那可以确定它属于nf_register_hook函数了。

    80060000 A _text

    ... ...

    80216b8c T nf_unregister_hooks

    80216be4 T nf_register_hook

    80216c8c T nf_register_hooks

    ... ...

    注意,System.map和内核启动后的/proc/kallsyms文件中的符号表只是给我们看的,内核不会使用它们。

    在由script/kallsyms工具生成的.tmp_kallsyms2.S文件中,kallsyms_addresses数组存放着所有符号的地址,并且是按照地址升序排列的,所以通过二分查找可以定位到0x80216bf4所在函数的起始地址是下面的这个条目:

    kallsyms_addresses:

       ... ...

        PTR _text + 0x1b6be4

       ... ...

    而这一项在kallsyms_addresses中的index为8801,所以现在需要找到kallsyms_names中的第8801个符号。

    我们这时实际上可以在kallsyms_names进行查找了,怎么找呢?我们先看一下kallsyms_names大致的样子:

    [cpp]  view plain  copy
    1. kallsyms_names:  
    2.     .byte0x04, 0x54, 0x7e, 0xc3, 0x74  
    3.     .byte0x08, 0xa0, 0x6b, 0xfa, 0xda, 0xbc, 0xe4, 0xe2, 0x79  
    4.     .byte0x09, 0xa0, 0x69, 0xd6, 0x93, 0x63, 0x6d, 0x64, 0xa5, 0x65  
    5.     .byte0x09, 0x54, 0xaa, 0x5f, 0xec, 0xfe, 0xc2, 0x63, 0xe7, 0x6c  
    6.     .byte0x09, 0x1a, 0x0d, 0x5f, 0xe3, 0xb2, 0xd3, 0x75, 0x75, 0xa4  
    7.     .byte0x07, 0x05, 0x61, 0x6d, 0xfe, 0x04, 0x95, 0x74  
    8.      ... ...  

    其中每一行存储一个压缩后的符号,而index和kallsyms_addresses中的index是一一对应的。每一行的内容分为两部分:第一个byte指明符号的长度,后续才是符号自身。虽然我们这里看到的符号是一行一行分开的,但实际上kallsyms_names是一个unsigned char的数组,所以想要找第8801个符号,只能这样来找:

    1. 从第一个字节开始,获得第一个符号的长度len;

    2. 向后移len+1个字节,就达到第二个符号的长度字节,这时记录下已经走过的总长度;

    3. 重复前两步的动作,直到走过的总长度为8801。

    这样找的话,要找到kallsyms_names的第8801个符号就要移动8801次,那如果要寻找最后一个符号,就要移动更多次,时间耗费较多,所以内核通过一个kallsyms_markers数组进行查找。

    将kallsyms_names每256个符号分为一组,每一组的第一个字符的位置记录在kallsyms_markers中,这样,我们在找kallsyms_names中的某个条目时,可以快速定义到它位于那个组,然后再在组内寻找,组内移动次数最多为255次。

    所以我们先通过(8801 >> 8)得到了要找的符号位于第34组,

    我们看到kallsyms_markers的第34项为:

        PTR 91280

    这个值指明了kallsyms_names中第34组的起始字符的偏移,所以我们直接找到kallsyms_names[91280]位置,即是第34组所有符号的第一个字节。同时我们可以通过(8801 && 0xFF)得到要找的符号在第34组组内的序号为97,即第97个符号。

    接下来寻找第97个符号就只能通过上面讲到的方法了。

    通过上面一系列的查找,我们定位到第34组中第97个符号如下:

    .byte 0x08, 0x05, 0x66, 0xdc, 0xb6, 0xc8, 0x68, 0x6f,0x0b

    这个是压缩后的符号,第一个字节0x08是符号长度,所以我们接下来的任务就剩下解压了。

    每个字节解压后对应的字符串在kallsyms_token_table中可以找到。于是在kallsyms_token_table表中寻找第5(0x05)项、第5(0x05)项、第102(0x66)项、……、第11(0x0b)项,得到的结果分别为:

    "Tn", "f", "_re","gist", "er_", "h", "o", "ok"

    由于在压缩的时候将符号类型“T”也压进去了,所以要去掉第一个字符,至此就获得了0x80216bf4地址所在的函数为nf_register_hook。

    参考自:http://blog.csdn.net/jasonchen_gbd/article/details/44025681

    3. 内核模块的符号

    内核模块是在内核启动过程中动态加载到内核中的,所以,不能试图将模块中的符号嵌入到vmlinux中。加载模块时,模块的符号表被存放在该模块的struct module结构中。所有已加载的模块的structmodule结构都放在一个全局链表中。

    在查找一个内核模块的符号时,调用的函数依然是kallsyms_lookup(),模块符号的实际查找工作在get_ksymbol()函数中完成。

    附录:一个.tmp_kallsyms2.S文件

    [cpp]  view plain  copy
    1. #include <asm/types.h>  
    2. #if BITS_PER_LONG == 64  
    3. #define PTR .quad  
    4. #define ALGN .align 8  
    5. #else  
    6. #define PTR .long  
    7. #define ALGN .align 4  
    8. #endif  
    9.    .section.rodata, "a"  
    10. .globl kallsyms_addresses  
    11.    ALGN  
    12. kallsyms_addresses:  
    13.    PTR _text + 0x400  
    14.    PTR _text + 0x400  
    15.    PTR _text + 0x410  
    16.    PTR _text + 0x810  
    17.    PTR _text + 0x9e0  
    18.    PTR _text + 0xa14  
    19.    PTR _text + 0xea0  
    20.    PTR _text + 0xec4  
    21.    PTR _text + 0xf00  
    22.    PTR _text + 0xf10  
    23.    ... ...  
    24.     
    25. .globl kallsyms_num_syms  
    26.    ALGN  
    27. kallsyms_num_syms:  
    28.    PTR 11132  
    29.    
    30. .globl kallsyms_names  
    31.    ALGN  
    32. kallsyms_names:  
    33.    .byte 0x04,0x54, 0x7e, 0xc3, 0x74  
    34.    .byte 0x08,0xa0, 0x6b, 0xfa, 0xda, 0xbc, 0xe4, 0xe2, 0x79  
    35.    .byte 0x09,0xa0, 0x69, 0xd6, 0x93, 0x63, 0x6d, 0x64, 0xa5, 0x65  
    36.    .byte 0x09,0x54, 0xaa, 0x5f, 0xec, 0xfe, 0xc2, 0x63, 0xe7, 0x6c  
    37.    .byte 0x09,0x1a, 0x0d, 0x5f, 0xe3, 0xb2, 0xd3, 0x75, 0x75, 0xa4  
    38.    .byte 0x07,0x05, 0x61, 0x6d, 0xfe, 0x04, 0x95, 0x74  
    39.    .byte 0x09,0x74, 0xf6, 0x68, 0x37, 0x39, 0x5f, 0x68, 0xe7, 0x74  
    40.    .byte 0x09,0x74, 0xf6, 0x68, 0x37, 0x39, 0xdc, 0xf1, 0xee, 0x74  
    41.    .byte 0x0b,0x54, 0xc4, 0x73, 0x79, 0xf1, 0x65, 0xcc, 0x74, 0x79, 0x70, 0x65  
    42.    ... ...  
    43.     
    44. .globl kallsyms_markers  
    45.    ALGN  
    46. kallsyms_markers:  
    47.    PTR 0  
    48.    PTR 2831  
    49.    PTR 5578  
    50.    PTR 8289  
    51.    PTR 10855  
    52.    PTR 13684  
    53.    PTR 16544  
    54.    PTR 19519  
    55.    PTR 22294  
    56.    PTR 25225  
    57.    PTR 27761  
    58.    PTR 30097  
    59.    ... ...  
    60.    
    61. .globl kallsyms_token_table  
    62.    ALGN  
    63. kallsyms_token_table:  
    64.    .asciz "end"  
    65.    .asciz "Tjffs2"  
    66.    .asciz "map_"  
    67.    .asciz "int"  
    68.    .asciz "to_"  
    69.    .asciz "Tn"  
    70.    .asciz "t__"  
    71.    .asciz "unregist"  
    72.    .asciz "tn"  
    73.    .asciz "yn"  
    74.    .asciz "Tf"  
    75.    ... ...  
    76.    
    77. .globl kallsyms_token_index  
    78.    ALGN  
    79. kallsyms_token_index:  
    80.    .short 0  
    81.    .short 4  
    82.    .short 11  
    83.    .short 16  
    84.    .short 20  
    85.    .short 24  
    86.    .short 27  
    87.    .short 31  
    88.    .short 40  
    89.    ... ...  
    展开全文
  • &gt; 转自:... by skyfly   在 Objective-C 的 Fondation 框架中 NSString 对象是复杂的存在,各种方式创建以及不同长度的字符串都会影响 NSString 对象在内存中所处的...

    > 转自:http://skyfly.xyz/2015/11/08/iOS/NSString%E7%9A%84%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/ 

    by skyfly

     

    在 Objective-C 的 Fondation 框架中 NSString 对象是很复杂的存在,各种方式创建以及不同长度的字符串都会影响 NSString 对象在内存中所处的位置。Objective-C 在运行时也对其做了很多优化。今天就来研究一下 NSString 这个复杂的对象。

    构建一些测试代码:

    为了观察 NSString 的内存管理情况,我选择关闭 ARC 使用 MRC 来进行测试。以观察其引用计数等状况。

    先写一个 Log 宏。

     

    1

     

    #define TLog(_var) ({ NSString *name = @#_var; NSLog(@"%@: %@ -> %p : %@ %lu", name, [_var class], _var, _var, [_var retainCount]); })

    NSString揭秘

    测试代码如下:

     

    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

     

    NSString *str1 = @"sa";

    TLog(str1);

    //str1: __NSCFConstantString -> 0x100001050 : sa 18446744073709551615

    NSString *str2 = [NSString stringWithString:@"sa"];

    TLog(str2);

    //str2: __NSCFConstantString -> 0x100001050 : sa 18446744073709551615

    NSString *str3 = @"1234567890";

    TLog(str3);

    //str3: __NSCFConstantString -> 0x100001110 : 1234567890 18446744073709551615

    NSString *str4 = [NSString stringWithFormat:@"sa"];

    TLog(str4);

    //str4: NSTaggedPointerString -> 0x617325 : sa 18446744073709551615

    NSString *str5 = [NSString stringWithFormat:@"sa"];

    TLog(str5);

    //str5: NSTaggedPointerString -> 0x617325 : sa 18446744073709551615

    NSString *str6 = [NSString stringWithFormat:@"123456789"];

    TLog(str6);

    //str6: NSTaggedPointerString -> 0x1ea1f72bb30ab195 : 123456789 18446744073709551615

    NSString *str7 = [NSString stringWithFormat:@"1234567890"];

    TLog(str7);

    //str7: __NSCFString -> 0x100300800 : 1234567890 1

    结果是很复杂的,按照产生对象的isa大致可以分为三种情况:

    1. 产生的对象是 __NSCFConstantString
    2. 产生的对象是 __NSCFString
    3. 产生的对象是 NSTaggedPointerString

    而且可以看到,在 MRC 下的引用计数也是不尽相同的:

    引用计数类型
    1__NSCFString
    18446744073709551615(2^64-1)NSTaggedPointerString, __NSCFConstantString

    这样的话就提出了几个疑问:

    • 三种类型分别是什么,有什么不同?
    • 三种类型的字符串指针分别是在什么情况下产生的?
    • 三种类型的字符串分别处于内存的那个区域?
    • 引用计数为什么会是18446744073709551615?

    三种类型分别是什么,分别是在什么情况下产生的,分别处于内存的那个区域?

    __NSCFConstantString

    字符串常量,是一种编译时常量,它的 retainCount 值很大,是 4294967295,在控制台打印出的数值则是 18446744073709551615==2^64-1,测试证明,即便对其进行 release 操作,retainCount 也不会产生任何变化。是创建之后便是放不掉的对象。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例。

    这种对象一般通过字面值 @"..."CFSTR("...") 或者 stringWithString: 方法(需要说明的是,这个方法在 iOS6 SDK 中已经被称为redundant,使用这个方法会产生一条编译器警告。这个方法等同于字面值创建的方法)产生。

    这种对象存储在字符串常量区。

    __NSCFString

    和 __NSCFConstantString 不同, __NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。

    通过 NSString 的 stringWithFormat 等方法创建的 NSString 对象一般都是这种类型。

    这种对象被存储在堆上。

    NSTaggedPointerString

    理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。从他的引用计数可以看出,这货也是一个释放不掉的单例常量对象。在运行时根据实际情况创建。

    对于 NSString 对象来讲,当非字面值常量数字,英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型,如果有中文或其他特殊符号(可能是非 ASCII 字符)存在的话则会直接成为 )__NSCFString 类型。

    这种对象被直接存储在指针的内容中,可以当作一种伪对象。

    0x01 引用计数为什么会是18446744073709551615?

    这个值意味着无限的retainCount,这个对象是不能被释放的。
    所有的 NSCFConstantString对象的retainCount都是这个值,这就意味着 NSCFConstantString不会被释放,使用第一种方法创建的NSString,如果值一样,无论写多少遍,都是同一个对象。而且这种对象可以直接用 == 来比较。

    分析NSString的 copy,retain,mutableCopy表现

    测试代码如下:

     

    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

     

    NSString *testOutput;

    NSString *str9 = @"as";

    TLog(str9);

    TLog(str9);

    [str9 retain];

    TLog(str9);

    NSString *str = [str9 copy];

    TLog(str);

    TLog(str9);

    str = [str9 mutableCopy];

    TLog(str);

    TLog(str9);

    NSString *str10 = [NSString stringWithFormat:@"as"];

    TLog(str10);

    [str10 retain];

    TLog(str10);

    str = [str10 copy];

    TLog(str);

    TLog(str10);

    str = [str10 mutableCopy];

    TLog(str);

    TLog(str10);

    NSString *str11 = [NSString stringWithFormat:@"1234567890"];

    TLog(str11);

    [str11 retain];

    TLog(str11);

    str = [str11 copy];

    TLog(str);

    TLog(str11);

    str =[str11 mutableCopy];

    TLog(str);

    TLog(str11);

    实验证明:copy 会使原来的对象引用计数加一,并拷贝对象地址给新的指针。
    mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1.

    展开全文
  • 2.4 Unicode字符集 Unicode字符集(1994年),是国际通用的全球化字符集,收录有世界多国家的文字。既然能表示更多的字符,就需要占用更多的字节,从百科上查到Unicode需要用2个字节。 从集合角度来看,ASCII是...

    一、博客背景

    我偶尔会接到把csv导入数据库的任务,我通常都是先用pd.read_csv读取文件数据,接着用df.to_sql导入数据库。有时read_csv会遇到不同的字符编码问题,我的解决方法通常是把常用的几种字符编码挨个试一下,哪种结果正确就选择哪一种。

    二、博客目的

    今天在这里,把我遇到的几种字符编码梳理汇总一下,方便自己和大家以后查询。

    三、可以参考的字符编码

    1、我在read_csv遇到过的字符编码

    这里先放一下我用read_csv遇到过的编码吧。

    reader = pd.read_csv(file_path
                             , sep='\t'
                             # , encoding='gb18030'
                             # , encoding='unicode_escape'
                             , encoding='utf-16'
                             # , encoding='utf-8'
                             # , nrows=5
                             , chunksize=20000
        )
    

    我们主要看encoding参数,其他参数这里不讨论。
    有的csv文件不加encoding参数也能顺利读取,有的用utf-8就行,但是遇到极个别刁钻的,则需要用其他编码方式。
    编码方式如果想要搞透彻需要多看几篇博客了,等我研究后再添加到文后。
    下面我之前遇到的编码方式的问题,如果你不幸也到了编码问题,就试下上面几种encoding,如果还不行就查阅其他网页吧。

    2、常见的bug

    UnicodeDecodeError: utf-8 codec can t decode byte 0xb3 in position 732: invalid start byte
    2、UnicodeDecodeError: ‘utf-8’ codec can’t decode byte 0xff in position 0: invalid start byte
    3、UnicodeDecodeError: ‘gb18030’ codec can’t decode byte 0xff in position 0: illegal multibyte sequence

    四、浅谈字符编码问题

    1、数字怎么编码

    一个bit可以是数字0或者是数字1,一个字节由8个bit组成,全是0表示数字0,全是1表示数字255,由排列组合可以计算出8个bit共有2^8=256种不同组合,所以一个字节可以表示数字0~255共256个数字。在计算机中,数字是这样编码。
    在这里插入图片描述

    2、字符怎么编码

    一堆二进制的0和1怎么也算不出字母A吧,那怎么用0和1表示字母A呢?直接表示行不通,就用间接的方法吧,用数字中转一下,给字母A指定一个数字编号,比如数字65(注:博客中所有字母或汉字的编号都是编造的,后续查到正确的再替换)。在计算机里,存储字母A时,存储数字65对应的二进制编码,要读取时,先把二进制编码转成数字编码,再查看数字编码对应的字母。
    在这里插入图片描述
    把符号和编号一一对应收录起来就是字符集了。

    2.1 ASCII字符集

    ASCII字符集(1967年)是美国人专门为英文设计的,收录了128个字符,包括大小写字母、数字0-9、标点符号、非打印字符(换行符、制表符等4个)以及控制字符(退格、响铃等)。
    前面讲到一个字节由8个bit组成,1个bit可以表示0或1共2个数字,由排列组合可以计算出8个bit可以组合出2^8=256个不同的组合,也就是一个字节可以表示256个数字。
    这么看来,一个字节就可以完全表示ASCII字符集里任一个字符了。
    没有中文怎么行?

    2.2 GB2312字符集

    GB2312字符集(1980年),国标,中国的字库,包括简体中文、拉丁字母、日文片假名。
    没有繁体字怎么行呢?

    2.3 BIG5字符集

    BIG5字符集(1984年),包含了繁体字。
    那其他国家的文字怎么办?

    2.4 Unicode字符集

    Unicode字符集(1994年),是国际通用的全球化字符集,收录有世界很多国家的文字。既然能表示更多的字符,就需要占用更多的字节,从百科上查到Unicode需要用2个字节。
    从集合角度来看,ASCII是Unicode的子集,能用ASCII表达的字符,如果用Unicode表达,前面一个字节就会用0填充,造成存储的浪费。
    我更倾向于这样理解,Unicode像个定长编码,能用1个字节表达的用了2个字节就造成了资源的浪费。
    此时需要更灵活的编码,UTF就产生了。

    2.5 UTF

    UTF(Unicode Transformation Format,Unicode转换格式或统一码转换格式),为了解决Unicode定长这个问题而生的,它是可变长度的字符编码,主要有UTF-8、UTF-16、UTF-32等。

    UTF-8

    1-6个字节组成,汉字由3个字节表示。
    (有时间再研究下UTF不同编码之间的联系和差异。)

    表格是我根据自己的理解总结的,后面的编码产生的原因都是为了弥补或补充前面编码的功能。

    五、愿景

    写完这个博客我有2个愿望,
    1、希望三、可以参考的字符编码能帮助你解决使用read_csv遇到的encoding问题。
    2、希望四、浅谈字符编码能帮助你理解字符编码,知道为什么会有这种字符编码出现以及它的优缺点。
    好了,暂时到这里吧,欢迎留言讨论。

    展开全文
  • 调用约定__cdecl、__stdcall和__fastcall的区别

    千次阅读 多人点赞 2018-08-26 21:12:08
    基于不同的需求,历史实践和开发人员的创造力,这些公司都使用了各自不同的调用约定,往往差异大。 在IBM兼容机市场洗牌后,微软操作系统和编程工具(有不同的调用约定)占据了统治地位,此时位于第二层次的公司如...

    什么是调用约定

    函数的调用约定,顾名思义就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。它决定以下内容:(1)函数参数的压栈顺序,(2)由调用者还是被调用者把参数弹出栈,(3)以及产生函数修饰名的方法。

    历史背景

    在微机出现之前,计算机厂商几乎都会提供一份操作系统和为不同编程语言编写的编译器。平台所使用的调用约定都是由厂商的软件实现定义的。 在Apple Ⅱ出现之前的早期微机几乎都是“裸机”,少有一份OS或编译器的,即是IBM PC也是如此。IBM PC兼容机的唯一的硬件标准是由Intel处理器(8086, 80386)定义的,并由IBM分发出去。硬件扩展和所有的软件标准(BIOS调用约定)都开放有市场竞争。 一群独立的软件公司提供了操作系统,不同语言的编译器以及一些应用软件。基于不同的需求,历史实践和开发人员的创造力,这些公司都使用了各自不同的调用约定,往往差异很大。 在IBM兼容机市场洗牌后,微软操作系统和编程工具(有不同的调用约定)占据了统治地位,此时位于第二层次的公司如Borland和Novell,以及开源项目如GCC,都还各自维护自己的标准。互操作性的规定最终被硬件供应商和软件产品所采纳,简化了选择可行标准的问题。

    调用者清理

    在这些约定中,调用者自己清理堆栈上的参数(arguments),这样就允许了可变参数列表的实现,如printf()。

    cdecl

    cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:

    1. 函数实参在线程栈上按照从右至左的顺序依次压栈。
    2. 函数结果保存在寄存器EAX/AX/AL中
    3. 浮点型结果存放在寄存器ST0中
    4. 编译后的函数名前缀以一个下划线字符
    5. 调用者负责从线程栈中弹出实参(即清栈)
    6. 8比特或者16比特长的整形实参提升为32比特长。
    7. 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
    8. 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
    9. RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)

    Visual C++规定函数返回值如果是POD值且长度如果不超过32比特,用寄存器EAX传递;长度在33-64比特范围内,用寄存器EAX:EDX传递;长度超过64比特或者非POD值,则调用者为函数返回值预先分配一个空间,把该空间的地址作为隐式参数传递给被调函数。

    GCC的函数返回值都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAX。GCC自4.5版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。

    考虑下面的C代码片段:

      int callee(int, int, int);
      int caller(void)
      {
          register int ret;
          
          ret = callee(1, 2, 3);
          ret += 5;
          return ret;
      }

    在x86上, 会产生如下汇编代码(AT&T 语法):

       .globl  caller
      caller:
            pushl   %ebp
            movl    %esp,%ebp
            pushl   $3
            pushl   $2
            pushl   $1
            call    callee
            addl    $12,%esp
            addl    $5,%eax
            leave
            ret

    在函数返回后,调用的函数清理了堆栈。 在cdecl的理解上存在一些不同,尤其是在如何返回值的问题上。结果,x86程序经过不同OS平台的不同编译器编译后,会有不兼容的情况,即使它们使用的都是“cdecl”规则并且不会使用系统调用。某些编译器返回简单的数据结构,长度大致占用两个寄存器,放在寄存器对EAX:EDX中;大点的结构和类对象需要异常处理器的一些特殊处理(如一个定义的构造函数,析构函数或赋值),存放在内存上。为了放置在内存上,调用者需要分配一些内存,并且让一个指针指向这块内存,这个指针就作为隐藏的第一个参数;被调用者使用这块内存并返回指针----返回时弹出隐藏的指针。 在Linux/GCC,浮点数值通过x87伪栈被推入堆栈。像这样:

       sub esp, 8      ; 给double值一点空间
            fld [ebp + x]   ; 加载double值到浮点堆栈上
            fstp [esp]      ; 推入堆栈
            call funct
            add esp, 8

    使用这种方法确保能以正确的格式推入堆栈。 cdecl调用约定通常作为x86 C编译器的默认调用规则,许多编译器也提供了自动切换调用约定的选项。如果需要手动指定调用规则为cdecl,编译器可能会支持如下语法:

      return_type _cdecl funct();

    其中_cdecl修饰符需要在函数原型中给出,在函数声明中会覆盖掉其他的设置。

    syscall

    与cdecl类似,参数被从右到左推入堆栈中。EAX, ECX和EDX不会保留值。参数列表的大小被放置在AL寄存器中(?)。 syscall是32位OS/2 API的标准。

    optlink

    参数也是从右到左被推入堆栈。从最左边开始的三个字符变元会被放置在EAX, EDX和ECX中,最多四个浮点变元会被传入ST(0)到ST(3)中----虽然这四个参数的空间也会在参数列表的栈上保留。函数的返回值在EAX或ST(0)中。保留的寄存器有EBP, EBX, ESI和EDI。 optlink在IBM VisualAge编译器中被使用。

    被调用者清理

    如果被调用者要清理栈上的参数,需要在编译阶段知道栈上有多少字节要处理。因此,此类的调用约定并不能兼容于可变参数列表,如printf()。然而,这种调用约定也许会更有效率,因为需要解堆栈的代码不要在每次调用时都生成一遍。 使用此规则的函数容易在asm代码被认出,因为它们会在返回前解堆栈。x86 ret指令允许一个可选的16位参数说明栈字节数,用来在返回给调用者之前解堆栈。代码类似如下:

     ret 12
    

    pascal

    基于Pascal语言的调用约定,参数从左至右入栈(与cdecl相反)。被调用者负责在返回前清理堆栈。 此调用约定常见在如下16-bit 平台的编译器:OS/2 1.x,微软Windows 3.x,以及Borland Delphi版本1.x。

    register

    Borland fastcall的别名。

    stdcall

    stdcall是由微软创建的调用约定,是Windows API的标准调用约定。非微软的编译器并不总是支持该调用协议。GCC编译器如下使用:

    int __attribute__((__stdcall__ )) func()

    stdcall是Pascal调用约定与cdecl调用约定的折衷:被调用者负责清理线程栈,参数从右往左入栈。其他各方面基本与cdecl相同。但是编译后的函数名后缀以符号"@",后跟传递的函数参数所占的栈空间的字节长度。寄存器EAX, ECX和EDX被指定在函数中使用,返回值放置在EAX中。stdcall对于微软Win32 API和Open Watcom C++是标准。

    微软的编译工具规定:PASCAL, WINAPI, APIENTRY, FORTRAN, CALLBACK, STDCALL, __far __pascal, __fortran, __stdcall均是指此种调用约定。

    fastcall

    此约定还未被标准化,不同编译器的实现也不一致。

    Microsoft/GCC fastcall

    Microsoft或GCC的__fastcall约定(也即__msfastcall)把第一个(从左至右)不超过32比特的参数通过寄存器ECX/CX/CL传递,第二个不超过32比特的参数通过寄存器EDX/DX/DL,其他参数按照自右到左顺序压栈传递。

    Borland fastcall

    从左至右,传入三个参数至EAX, EDX和ECX中。剩下的参数推入栈,也是从左至右。 在32位编译器Embarcadero Delphi中,这是缺省调用约定,在编译器中以register形式为人知。 在i386上的某些版本Linux也使用了此约定。

    调用者或被调用者清理

    thiscall

    在调用C++非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall。 对于GCC编译器,thiscall几乎与cdecl等同:调用者清理堆栈,参数从右到左传递。差别在于this指针,thiscall会在最后把this指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。

    微软Visual C++编译器中,this指针通过ECX寄存器传递,其余同cdecl约定。当函数使用可变参数,此时调用者负责清理堆栈(参考cdecl)。thiscall约定只在微软Visual C++ 2005及其之后的版本被显式指定。其他编译器中,thiscall并不是一个关键字(反汇编器如IDA使用__thiscall)。

    Intel ABI

    根据Intel ABI,EAX、EDX及ECX可以自由在过程或函数中使用,不需要保留。

    x86-64调用约定

    x86-64调用约定得益于更多的寄存器可以用来传参。而且,不兼容的调用约定也更少了,不过还是有2种主流的规则。

    微软x64调用约定

    微软x64调用约定使用RCX, RDX, R8, R9这四个寄存器传递头四个整型或指针变量(从左到右),使用XMM0, XMM1, XMM2, XMM3来传递浮点变量。其他的参数直接入栈(从右至左)。整型返回值放置在RAX中,浮点返回值在XMM0中。少于64位的参数并没有做零扩展,此时高位充斥着垃圾。 在Windows x64环境下编译代码时,只有一种调用约定----就是上面描述的约定,也就是说,32位下的各种约定在64位下统一成一种了。 在微软x64调用约定中,调用者的一个职责是在调用函数之前(无论实际的传参使用多大空间),在栈上的函数返回地址之上(靠近栈顶)分配一个32字节的“影子空间”;并且在调用结束后从栈上弹掉此空间。影子空间是用来给RCX, RDX, R8和R9提供保存值的空间,即使是对于少于四个参数的函数也要分配这32个字节。

    例如, 一个函数拥有5个整型参数,第一个到第四个放在寄存器中,第五个就被推到影子空间之外的栈顶。当函数被调用,此栈用来组成返回值----影子空间32位+第五个参数。

    在x86-64体系下,Visual Studio 2008在XMM6和XMM7中(同样的有XMM8到XMM15)存储浮点数。结果对于用户写的汇编语言例程,必须保存XMM6和XMM7(x86不用保存这两个寄存器),这也就是说,在x86和x86-64之间移植汇编例程时,需要注意在函数调用之前/之后,要保存/恢复XMM6和XMM7。

    System V AMD64 ABI

    此约定主要在Solaris,GNU/Linux,FreeBSD和其他非微软OS上使用。头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上;同时XMM0到XMM7用来放置浮点变元。对于系统调用,R10用来替代RCX。同微软x64约定一样,其他额外的参数推入栈,返回值保存在RAX中。 与微软不同的是,不需要提供影子空间。在函数入口,返回值与栈上第七个整型参数相邻。

    以上内容来源中文维基:https://zh.wikipedia.org/zh-hans/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A

     

    我们知道函数由以下几部分构成:返回值类型 函数名(参数列表),如: 
    【code1】

    void function();
    int add(int a, int b);
    

    以上是大家所熟知的构成部分,其实函数的构成还有一部分,那就是调用约定。如下: 
    【code2】

    void __cdecl function();
    int __stdcall add(int a, int b);
    

    上面的__cdecl和__stdcall就是调用约定,其中__cdecl是C和C++默认的调用约定,所以通常我们的代码都如 【code1】中那样定义,编译器默认会为我们使用__cdecl调用约定。常见的调用约定有__cdecl、__stdcall、fastcall,应用最广泛的是__cdecl和__stdcall,下面我们会详细进行讲述。。还有一些不常见的,如 __pascal、__thiscall、__vectorcall。

    声明和定义处调用约定必须要相同

    在VC++中,调用约定是函数类型的一部分,因此函数的声明和定义处调用约定要相同,不能只在声明处有调用约定,而定义处没有或与声明不同。如下: 
    【code3】 错误的使用一:

    int __stdcall add(int a, int b);
    int add(int a, int b)
    {
        return a + b;
    }
    

    报错:

    error C2373: ‘add’: redefinition; different type modifiers 
    error C2440: ‘initializing’: cannot convert from ‘int (__stdcall *)(int,int)’ to ‘int’

    补充:

    int __cdecl add(int a, int b);
    int add(int a, int b)
    {
        return a + b;
    }
    

    以上就没问题,因为默认是__cdecl。

    【code4】 错误的使用二:

    int  add(int a, int b);
    int __stdcall add(int a, int b)
    {
        return a + b;
    }
    

    报错:

    error C2373: ‘add’: redefinition; different type modifiers 
    error C2440: ‘initializing’: cannot convert from ‘int (__cdecl *)(int,int)’ to ‘int’

    【code5】 错误的使用三:

    int __stdcall add(int a, int b);
    int __cdecl add(int a, int b)
    {
        return a + b;
    }
    

    报错:

    error C2373: ‘add’: redefinition; different type modifiers 
    error C2440: ‘initializing’: cannot convert from ‘int (__stdcall *)(int,int)’ to ‘int’

    【code6】 正确的用法:

    int __stdcall add(int a, int b);
    int __stdcall add(int a, int b)
    {
        return a + b;
    }
    

    函数的调用过程

    要深入理解函数调用约定,你须要了解函数的调用过程和调用细节。 
    假设函数A调用函数B,我们称A函数为”调用者”,B函数为“被调用者”。如下面的代码,ShowResult为调用者,add为被调用者。

    int add(int a, int b)
    {
        return a + b;
    }
    
    void ShowResult()
    {
        std::cout << add(5, 10) << std::endl;
    }
    

    函数调用过程可以这么描述: 
    (1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。 
    (2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。 
    (3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。 
    (4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。 
    这个过程在AT&T汇编中通过两条指令完成,即: 

       leave
       ret
      这两条指令更直白点就相当于:
      mov   %ebp , %esp
      pop    %ebp
    

     

    __cdecl的特点

    __cdecl 是 C Declaration 的缩写,表示 C 和 C++ 默认的函数调用约定。是C/C++和MFCX的默认调用约定。

    • 按从右至左的顺序压参数入栈、。
    • 由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的,返回值在EAX中。因此对于像printf这样可变参数的函数必须用这种约定。
    • 编译器在编译的时候对这种调用规则的函数生成修饰名的时候,在输出函数名前加上一个下划线前缀,格式为_function。如函数int add(int a, int b)的修饰名是_add。

    (1).为了验证参数是从右至左的顺序压栈的,我们可以看下面这段代码,Debug进行单步调试,可以看到我们的调用栈会先进入GetC(),再进入GetB(),最后进入GetA()。 

    (2).第二点“调用者把参数弹出栈”,这是编译器的工作,暂时没办法验证。要深入了解这部分,需要学习汇编语言相关的知识。

    (3).函数的修饰名,这个可以通过对编译出的dll使用VS的”dumpbin /exports ProjectName.dll”命令进行查看(后面章节会进行详细介绍),或直接打开.obj文件查找对应的方法名(如搜索add)。

    从代码和程序调试的层面考虑,参数的压栈顺序和栈的清理我们都不用太观注,因为这是编译器的决定的,我们改变不了。但第三点却常常困扰我们,因为如果不弄清楚这点,在多个库之间(如dll、lib、exe)相互调用、依赖时常常出出现莫名其妙的错误。这个我在后面章节会进行详细介绍。

    __stdcall的特点

    __stdcall是Standard Call的缩写,是C++的标准调用方式,当然这是微软定义的标准,__stdcall通常用于Win32 API中(可查看WINAPI的定义)。   microsoft的vc默认的是__cdecl方式,而windows API则是__stdcall,如果用vc开发dll给其他语言用,则应该指定__stdcall方式。堆栈由谁清除这个很重要,如果是要写汇编函数给C调用,一定要小心堆栈的清除工作,如果是__cdecl方式的函数,则函数本身(如果不用汇编写)则不需要关心保存参数的堆栈的清除,但是如果是__stdcall的规则,一定要在函数退出(ret)前恢复堆栈。

    • 按从右至左的顺序压参数入栈。
    • 由被调用者把参数弹出栈。切记:函数自己在退出时清空堆栈,返回值在EAX中。
    • __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_function@number。如函数int sub(int a, int b)的修饰名是_sub@8。

    __fastcall的特点

    __fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的。

    • 实际上__fastcall用ECX和EDX传送前两个DWORD或更小的参数,剩下的参数仍自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈。
    • __fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@function@number,如double multi(double a, double b)的修饰名是@multi@16。
    • __fastcall和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第1个参数进ECX,第2个进EDX,其他参数是从右向左的入栈,返回仍然通过EAX。

    __thiscall

    __thiscall是C++类成员函数缺省的调用约定,但它没有显示的声明形式。因为在C++类中,成员函数调用还有一个this指针参数,因此必须特殊处理,thiscall调用约定的特点:

    • 参数入栈:参数从右向左入栈
    • this指针入栈:如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入栈。
    • 栈恢复:对参数个数不定的,调用者清理栈,否则函数自己清理栈。

    总结

    这里主要总结一下_cdecl、_stdcall、__fastcall三者之间的区别:

    要点__cdecl__stdcall__fastcall
    参数传递方式右->左右->左左边开始的两个不大于4字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数自右向左压栈传送
    清理栈方调用者清理被调用函数清理被调用函数清理
    适用场合C/C++、MFC的默认方式; 可变参数的时候使用;Win API要求速度快
    C编译修饰约定_functionname_functionname@number@functionname@number

    以上内容参考:https://blog.csdn.net/luoweifu/article/details/52425733#commentBox

     

    展开全文
  • 1. 作用:处理字符时,强大 2. 分类:与下列相似,当功能更加强大('支持正则表达式') (1) regexp_like : 同 like 功能相似(模糊 '匹配') (2) regexp_instr : 同 instr 功能相似(返回字符所在 '下标') (3...
  • Oracle中RegExp_Like 正则表达式基本用法

    万次阅读 多人点赞 2019-11-06 10:56:00
    评注:网上流传的版本功能有限,上面这个基本可以满足需求 匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$ 评注:表单验证时实用 匹配国内...
  • Swift int8_t 、int_fast8_t、int8区别

    千次阅读 2019-05-08 19:34:20
    在刚开始学习swift 时 如果你没有其他语言基础,难理解,为什么一个int类型好想衍生出了,那么多其他的数据类型,例如下图所展示的 这些东西究竟是个什么东西,这里我来说明一下 这些是 c 语言 和 c++ 语言中的 ...
  • 在头文件include/linux/sched.h文件里面涉及到多与重新调度标识相关的函数实现,比如test/set/clear等等操作.可以看到下面这个函数: /* * resched_curr - mark rq's current task 'to be rescheduled now'. *...
  • MySQL-使用UUID_SHORT( ) 的问题

    万次阅读 2019-04-08 11:58:48
    而且,由于使用到了时间搓,这个的初始值会大(通常都会到17位数字)。 但是,这并不保证其不会返回18,19,20位的数据,只能保证在2^64-1以内(最大达到20位数字); 目前没有查到说可以调整或初始设置UUID...
  • 这有个优点,能支持很长的字符串拼接,短了可以TO_CHAR展示 但也有缺点,就是大大增大了临时段的读写,数据量大时可能会出现错误 ORA-01652:unable to extend temp segment by 128 in tablespace name ( 无法...
  • * 由于在做数据处理,数据分析的时候,免不了读取数据或者将数据转换为相应的处理形式,那么,pandas的read_csv和to_csv,就能给我们大的帮助,接下来,博主,将 read_csv 和 to_csv 两个方法的定义,进行整合,...
  • sscanf和sscanf_s使用方法

    千次阅读 2018-06-03 22:12:42
    在使用VS2005编译一个程序时,出现了多警告,说是用的函数是不安全的,应当使用安全版本,即函数名称增加“_s”的版本。    警告内容:   warning C4996: 'sscanf': This function or variable may be unsafe. ...
  • ,即上面应该等价于为(unsigned long)(-MAX_ERRNO),-0xfff转换为无符号long型是0xfffff000,即: # define IS_ERR_VALUE(x) unlikely(x >= 0xfffff000)  即判断是不是在 (0xfffff000,0xffffffff) ...
  • C语言中size_t类型详细说明【转载】

    万次阅读 多人点赞 2018-02-28 16:06:13
    在c语言的多库函数中,函数原型中,参数类型都是size_t。但是在我们编写程序时size_t类型却少有所使用。那么这个类型到底有什么作用呢使用size_t可能会提高代码的可移植性、有效性或者可读性,或许同时提高这三...
  • pandas.read_csv学习笔记

    万次阅读 多人点赞 2018-03-16 09:24:35
    pandas.read_csv功能简单,就是读取csv文本文件到DataFrame变量中。就是参数比较多。pandas.read_csv(filepath_or_buffer, sep=', ', delimiter=None, header='infer', names=None, index_col=None, usecols=None,...
  • 深入理解dpdk rte_ring无锁队列

    万次阅读 2017-04-06 00:05:42
    图解有符号和无符号数隐藏的含义 ”解释了二进制的回环性,无符号数计算距离的技巧。根据二进制的回环性,可以直接用(uint32_t)( prod_tail - cons_tail)计算队列中有多少生产的产品(即使溢出了也不会出错,如...
  • 函数main_loop和u-boot命令执行

    千次阅读 2016-09-21 09:11:14
    参数_type为cmd_tbl_t,这里定义一个cmd_tbl_t结构体,并把它放在符号段.u_boot_list_2_"#_list"_2_"#_name中, 其中的_list和_name根据宏参数进行字符串替换。 下面,我们举例说明上述宏的实现机制。比如有如下的...
  • C语言第十七篇:size_t 数据类型

    万次阅读 2016-05-21 01:19:50
    size_t 用做sizeof 操作符的返回值类型,同时也是多函数的参数类型,包括malloc 和strlen。 在声明诸如字符数或者数组索引这样的长度变量时用size_t 是好的做法。它经常用于循环计数器、数组索引,有时候...
  • 今天来记录一下Oracle中如何使用正则表达式。 一、正则表达式匹配规则。 在别人那里看到一篇文章,这里对匹配规则记录的可以说是...明显,一个是正则表达式,一个不是。。。如果我这么说的话,你会不会骂我。。
  • 其实还是没有讲的清楚,不过看了下面的例子,一定非常清楚: #include <stdio.h> #include <unistd.h> #include <sys/select.h> #include <errno.h> #include <sys/inotify.h>   static...
  •  这里提供三种方法将图像数据转存为TFrecords数据格式,当然也包含TFrecords解析的方法,详细的用法都会在函数参数说明,已经封装了简单了,你只需要改变你图片的路径就可以。 生成单个record文件 (单label)...
  • 代码稍微有点,但是为了保留kernel源码的美感,所以上面代码没有做任何改动。 2.1 bus_add_driver简化过程 为了使分析bus_add_driver不显得太杂乱,这里我将bus_add_driver分为以下几个部分: int ...
  • group_concat的用法 及注意点

    千次阅读 2016-05-18 09:49:34
    程序中进行这项操作的语法如下,其中 val 是一个无符号整数: SET [SESSION | GLOBAL] group_concat_max_len = val; 若已经设置了最大长度, 则结果被截至这个最大长度。 在SQLyog中执行 SET GLOBAL group_concat...
  • 多资料讲了关于TCP的CLOSING和CLOSE_WAIT状态以及所谓的优雅关闭的细节,多数侧重与Linux的内核实现(除了《UNIX网络编程》)。本文不注重代码细节,只关注逻辑。所使用的工具,tcpdump,packetdrill以及ss。 关于...
  • 详解sprintf()&sprintf_s()

    万次阅读 多人点赞 2015-03-12 15:03:58
    sprintf 函数功能:把格式化的数据写入某个字符串  头文件:stdio.h  函数原型:int sprintf( char *...在将各种类型的数据构造成字符串时,sprintf 的功能强大。sprintf 与printf 在用法上几乎一样,只是打印
  • 1、明确类型定义 ...u:代表 unsigned 即无符号,即定义的变量不能为负数; int:代表类型为 int 整形; 8:代表一个字节,即为 char 类型; _t:代表用 typedef 定义的; 整体代表:用 typedef 定义的...
  • 浏览包含主函数的nginx.c文件,发现nginx使用了多自行封装的数据结构,不弄清楚这是些什么样的数据结构就难理解主函数中操作的意义。于是我们挑看起来基础的数据结构开始研究。组织nginx所有数据结构的是ngx_...
  • 通过作为多个前导码的组块中的互相关来检测前导序列符号很长。 使用的块大小由PAC大小配置选择,应该是 根据预期的前导码大小选择。 更大的PAC尺寸可以提供更好的性能序言足够长,允许它。 但是如果PAC的大小...
  • m_map中文文档

    万次阅读 多人点赞 2014-08-05 22:20:27
    各个选项的准确含义在第2节介绍,然而要注意到,精度是用带符号的形式表示的-西经为负值,东经为正值,同时应注意到,使用的是十进制度的形式,即西经120°30′应该表示为-120.5°。 第二行代码是画海岸线,使用1...
  • 程序出链接错误的时候,经常看到lnk errorxxx:某某函数、某某变量找不到等等,里面的函数名通常都难看明白,因为使用的是修饰名。 C 和 C++ 程序中的函数在内部通过其修饰名加以识别。修饰名是在编译函数定义或...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 484,813
精华内容 193,925
关键字:

很长的符号____________