【发布时间】:2010-10-11 01:11:31
【问题描述】:
只是想了解如何处理字符串数组中的单个字符。此外,这当然可以让我理解指向一般下标指针的指针。
如果我有char **a 并且我想到达第二个字符串的第三个字符,这是否有效:**((a+1)+2)?好像应该...
【问题讨论】:
只是想了解如何处理字符串数组中的单个字符。此外,这当然可以让我理解指向一般下标指针的指针。
如果我有char **a 并且我想到达第二个字符串的第三个字符,这是否有效:**((a+1)+2)?好像应该...
【问题讨论】:
差不多,但不完全。正确答案是:
*((*(a+1))+2)
因为您需要首先取消对实际字符串指针之一的引用,然后再将该选定的字符串指针取消引用到所需的字符。 (请注意,为了清楚起见,我在此处添加了额外的括号)。
或者,这个表达式:
a[1][2]
也可以!....也许会更受欢迎,因为您尝试做的事情的意图更加不言自明,并且符号本身更加简洁。这种形式对于刚接触该语言的人来说可能不是很明显,但要理解数组表示法起作用的原因是因为在 C 中,数组索引操作实际上只是等效指针操作的简写。即:*(a+x) 与 a[x] 相同。因此,通过将该逻辑扩展到原始问题,有两个单独的指针取消引用操作级联在一起,其中表达式 a[x][y] 等效于 *((*(a+x))+ 的一般形式y)。
【讨论】:
您不必使用指针。
int main(int argc, char **argv){
printf("第三个字符 argv[1] 是 [%c].\n", argv[1][2]);
}
然后:
$ ./main hello 第三个字符 argv[1] 是 [l]。
那是一加一。
如果你愿意,你可以使用指针...
*(argv[1] +2)
甚至
*((*(a+1))+2)
正如上面有人指出的那样。
这是因为数组名是指针。
【讨论】:
乔恩·埃里克森 (Jon Erickson) 所著的《Hacking the art of exploit 2nd Edition》一书中有一个精彩的 C 编程解释,其中讨论了指针、字符串,仅在编程解释部分就值得一提 https://leaksource.files.wordpress.com/2014 /08/hacking-the-art-of-exploitation.pdf.
虽然问题已经得到解答,但其他想了解更多信息的人可能会发现 Ericksons 书中的以下要点有助于理解问题背后的一些结构。
标题
您可能会使用的可用于变量操作的头文件示例。
stdio.h - http://www.cplusplus.com/reference/cstdio/
stdlib.h - http://www.cplusplus.com/reference/cstdlib/
string.h - http://www.cplusplus.com/reference/cstring/
limits.h - http://www.cplusplus.com/reference/climits/
函数
您可能会使用的通用函数示例。
malloc() - http://www.cplusplus.com/reference/cstdlib/malloc/
calloc() - http://www.cplusplus.com/reference/cstdlib/calloc/
strcpy() - http://www.cplusplus.com/reference/cstring/strcpy/
内存
"一个编译后的程序的内存被分为五段:text、data、bss、heap、stack。每段代表内存的一个特殊部分,为某种目的而留出。文本段也是有时称为代码段。这是程序的汇编机器语言指令所在的位置”。
"由于前面提到的高级控制结构和函数,该段中的指令执行是非线性的 汇编语言中的分支、跳转和调用指令。作为一个程序 执行时,EIP 被设置为文本段中的第一条指令。这 处理器然后遵循执行以下操作的执行循环:“
"1.读取EIP指向的指令"
"2.将指令的字节长度添加到EIP"
"3.执行步骤1中读取的指令"
"4. 回到步骤 1"
"有时指令会是跳转指令或调用指令, 将 EIP 更改为不同的内存地址。处理器不 关心变化,因为它期望执行是非线性的 反正。如果在第 3 步中更改了 EIP,处理器将返回第 1 步并读取在任何 EIP 更改为的地址处找到的指令"。
"文本段中的写权限被禁用,因为它不用于存储变量,仅用于存储代码。这会阻止人们实际修改程序代码;任何尝试写入该内存段都会导致程序来提醒用户发生了不好的事情,以及程序 将被杀死。该段为只读的另一个优点是它 可以在程序的不同副本之间共享,允许多个 同时执行程序没有任何问题。这应该 还要注意的是,这个内存段有一个固定的大小,因为从来没有 变化”。
"data和bss段用于存放全局和静态程序 变量。数据段填充了初始化的全局变量和静态变量,而 bss 段填充了未初始化的对应变量。尽管这些段是可写的,但它们也有固定的大小。请记住,尽管有函数上下文(如前面示例中的变量 j),全局变量仍然存在。全局变量和静态变量都能够持久化,因为它们存储在自己的内存段中"。
"堆段是程序员可以直接使用的一段内存 控制。该段中的内存块可以分配并用于 无论程序员可能需要什么。关于堆的一个值得注意的点 段是它不是固定大小的,所以它可以根据需要变大或变小“。
"堆中的所有内存都由分配器和释放器算法管理,它们分别在堆中保留一个内存区域以供使用并删除保留以允许该部分内存被重用以供以后保留。堆将根据如何增长和缩小 保留大量内存供使用。这意味着程序员使用堆 分配函数可以即时保留和释放内存。的成长 堆向下移动到更高的内存地址“。
"堆栈段的大小也是可变的,在函数调用期间用作临时暂存器来存储局部函数变量和上下文。这就是 GDB 的回溯命令查看的内容。当程序调用函数时,函数将有自己的一组传递变量,并且函数的代码将在文本(或代码)段中的不同内存位置。由于调用函数时上下文和EIP必须改变,因此堆栈用于记住所有传递的变量,函数完成后 EIP 应该返回的位置,以及该函数使用的所有局部变量。所有这些信息一起存储在堆栈中,统称为堆栈帧。堆栈包含许多堆栈帧"。
"在一般计算机科学术语中,堆栈是一种经常使用的抽象数据结构。它具有先进后出 (FILO) 顺序 ,这意味着放入堆栈的第一个项目是最后一个出来的项目。可以把它想象成将珠子放在一根一端有结的绳子上——除非你把所有其他珠子都拿掉,否则你不能取下第一个珠子。当一个项目被放入堆栈时,它被称为推送,当一个项目从堆栈中被移除时,它被称为弹出“。
"顾名思义,内存的栈段,其实就是一个栈数据结构,里面包含栈帧。ESP寄存器用来跟踪栈尾的地址,随着项目被推入和弹出,它会不断变化。由于这是非常动态的行为,因此堆栈也不是固定大小是有道理的。与堆的动态增长相反,随着堆栈的变化 s 的大小,它在内存的视觉列表中向上增长,向更低的内存地址增长"。
"堆栈的 FILO 特性可能看起来很奇怪,但由于使用了堆栈 存储上下文,它非常有用。当一个函数被调用时,几个东西在一个堆栈帧中一起被压入堆栈。 EBP 寄存器——有时称为帧指针 (FP) 或本地基 (LB) 指针 — 用于引用当前栈帧中的局部函数变量。每个堆栈帧都包含函数的参数、它的局部变量和两个将事物恢复原状所必需的指针:保存的帧指针 (SFP) 和返回地址。这 SFP用于将EBP恢复到之前的值,返回地址 用于将 EIP 恢复到函数调用后找到的下一条指令。这恢复了前一个堆栈的功能上下文 框架”。
字符串
"在 C 中,数组只是特定数据类型的 n 个元素的列表。20 个字符的数组就是位于内存中的 20 个相邻字符。数组也称为缓冲区 ”。
#include <stdio.h>
int main()
{
char str_a[20];
str_a[0] = 'H';
str_a[1] = 'e';
str_a[2] = 'l';
str_a[3] = 'l';
str_a[4] = 'o';
str_a[5] = ',';
str_a[6] = ' ';
str_a[7] = 'w';
str_a[8] = 'o';
str_a[9] = 'r';
str_a[10] = 'l';
str_a[11] = 'd';
str_a[12] = '!';
str_a[13] = '\n';
str_a[14] = 0;
printf(str_a);
}
"在前面的程序中,一个20元素的字符数组被定义为 str_a,并且将数组的每个元素一个一个地写入。请注意,数字从 0 开始,而不是 1。还要注意最后一个字符是 0"。
"(这个也叫空字节。) 定义了字符数组,所以给它分配了20个字节,但实际上只用到了这些字节中的12个。使用了最后的空字节编程作为分隔符告诉任何正在处理字符串的函数在此处停止操作。剩余的额外字节只是垃圾,将被忽略。如果在字符数组的第五个元素中插入空字节,则只有字符你好会被 printf() 函数打印出来".
"由于在字符数组中设置每个字符很费力,而且字符串也经常使用,因此为字符串操作创建了一组标准函数。例如,strcpy() 函数将从源中复制字符串到目的地,遍历源字符串并将每个字节复制到目的地(并在复制空终止字节后停止)“。
"函数参数的顺序类似于 Intel 汇编语法先目标后源。char_array.c 程序可以使用 strcpy() 重写以使用字符串库完成相同的事情。下一个版本下面显示的 char_array 程序包含 string.h,因为它使用字符串函数"。
#include <stdio.h>
#include <string.h>
int main()
{
char str_a[20];
strcpy(str_a, "Hello, world!\n");
printf(str_a);
}
查找有关 C 字符串的更多信息
http://www.cs.uic.edu/~jbell/CourseNotes/C_Programming/CharacterStrings.html
http://www.tutorialspoint.com/cprogramming/c_strings.htm
指针
"EIP 寄存器是一个指针,在程序执行期间通过包含其内存地址“指向”当前指令。在 C 中也使用了指针的概念。由于物理内存实际上无法移动,其中的信息必须被复制。复制大块内存以供不同的功能或在不同的地方使用可能在计算上非常昂贵。从内存的角度来看,这也是昂贵的,因为新目标副本的空间必须是在复制源之前保存或分配。指针是解决此问题的一种方法。与复制大块内存相比,传递该内存块开头的地址要简单得多“。
"C 中的指针可以像任何其他变量类型一样定义和使用。由于 x86 架构上的内存使用 32 位寻址,因此指针的大小也是 32 位(4 字节)。指针通过前置定义变量名称的星号 (*)。不是定义该类型的变量,而是将指针定义为指向该类型数据的东西。pointer.c 程序是指针与 char 数据一起使用的示例类型,大小只有 1 个字节”。
#include <stdio.h>
#include <string.h>
int main()
{
char str_a[20]; // A 20-element character array
char *pointer; // A pointer, meant for a character array
char *pointer2; // And yet another one
strcpy(str_a, "Hello, world!\n");
pointer = str_a; // Set the first pointer to the start of the array.
printf(pointer);
pointer2 = pointer + 2; // Set the second one 2 bytes further in.
printf(pointer2); // Print it.
strcpy(pointer2, "y you guys!\n"); // Copy into that spot.
printf(pointer); // Print again.
}
"正如代码中的cmets所指出的,第一个指针设置在字符数组的开头。当这样引用字符数组时,它实际上是一个指针本身。这个缓冲区是这样的之前作为指针传递给 printf() 和 strcpy() 函数。第二个指针设置为第一个指针地址加 2,然后打印一些东西(如下面的输出所示)"。 p>
reader@hacking:~/booksrc $ gcc -o pointer pointer.c
reader@hacking:~/booksrc $ ./pointer
Hello, world!
llo, world!
Hey you guys!
reader@hacking:~/booksrc $
"address-of 运算符通常与指针一起使用,因为指针包含内存地址。addressof.c 程序演示 用于放置整数变量地址的地址运算符 成一个指针。此行在下面以粗体显示"。
#include <stdio.h>
int main()
{
int int_var = 5;
int *int_ptr;
int_ptr = &int_var; // put the address of int_var into int_ptr
}
"存在一个额外的一元运算符,称为解引用运算符,用于与指针一起使用。此运算符将返回在指针指向的地址中找到的数据,而不是地址本身。它采用星号的形式在变量名前面,类似于指针的声明。再一次,解引用运算符在 GDB 和 C 中都存在"。
"对 addressof.c 代码(显示在 addressof2.c 中)的一些添加将 演示所有这些概念。添加的 printf() 函数使用格式 参数,我将在下一节中解释。现在,只关注程序输出“。
#include <stdio.h>
int main()
{
int int_var = 5;
int *int_ptr;
int_ptr = &int_var; // Put the address of int_var into int_ptr.
printf("int_ptr = 0x%08x\n", int_ptr);
printf("&int_ptr = 0x%08x\n", &int_ptr);
printf("*int_ptr = 0x%08x\n\n", *int_ptr);
printf("int_var is located at 0x%08x and contains %d\n", &int_var, int_var);
printf("int_ptr is located at 0x%08x, contains 0x%08x, and points to %d\n\n", &int_ptr, int_ptr, *int_ptr);
}
“当一元运算符与指针一起使用时,地址运算符可以被认为是向后移动,而解引用运算符则朝着指针指向的方向向前移动”。 p>
了解有关指针和内存分配的更多信息
Dan Hirschberg 教授,加州大学计算机科学系计算机内存 https://www.ics.uci.edu/~dan/class/165/notes/memory.html
http://cslibrary.stanford.edu/106/
http://www.programiz.com/c-programming/c-dynamic-memory-allocation
数组
这里 http://www.cprogramming.com/tutorial/c/lesson8.html 有一个名为 Alex Allian 的小伙子编写的关于多维数组的简单教程
这里有一个名叫 Todd A Gibson 的小伙子提供的关于数组的信息 http://www.augustcouncil.com/~tgibson/tutorial/arr.html
迭代数组
#include <stdio.h>
int main()
{
int i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
char *char_pointer;
int *int_pointer;
char_pointer = char_array;
int_pointer = int_array;
for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.
printf("[integer pointer] points to %p, which contains the integer %d\n", int_pointer, *int_pointer);
int_pointer = int_pointer + 1;
}
for(i=0; i < 5; i++) { // Iterate through the char array with the char_pointer.
printf("[char pointer] points to %p, which contains the char '%c'\n", char_pointer, *char_pointer);
char_pointer = char_pointer + 1;
}
}
链表与数组
数组不是唯一可用的选项,链表上的信息。
http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_linklist.aspx
结论
编写此信息只是为了传递我在整个研究过程中所读到的一些内容,这些内容可能对其他人有所帮助。
【讨论】:
Iirc,字符串实际上是一个字符数组,所以应该可以:
a[1][2]
【讨论】:
引用维基百科 article 关于 C 指针的内容 -
在 C 中,数组索引是用指针运算形式定义的;那是, 语言规范要求 array[i] 等价于 *(array + i)。因此,在 C 语言中,数组可以被认为是指向连续内存区域的指针(没有间隙), 访问数组的语法与可用于取消引用的语法相同 指针。例如,可以通过以下方式声明和使用数组:
int array[5]; /* Declares 5 contiguous (per Plauger Standard C 1992) integers */
int *ptr = array; /* Arrays can be used as pointers */
ptr[0] = 1; /* Pointers can be indexed with array syntax */
*(array + 1) = 2; /* Arrays can be dereferenced with pointer syntax */
所以,回答您的问题 - 是的,指向指针的指针可以用作数组,而无需任何其他声明!
【讨论】:
试试a[1][2]。或*(*(a+1)+2)。
基本上,数组引用是指针解引用的语法糖。 a[2] 与 a+2 相同,也与 2[a] 相同(如果您真的想要不可读的代码)。字符串数组与双指针相同。因此,您可以使用 a[1] 或 *(a+1) 提取第二个字符串。然后,您可以使用 b[2] 或 *(b + 2) 找到该字符串中的第三个字符(现在称为“b”)。将原始的第二个字符串替换为 'b',我们最终得到 a[1][2] 或 *(*(a+1)+2)。
【讨论】: