【问题标题】:How many bytes are there in one address? [duplicate]一个地址有多少字节? [复制]
【发布时间】:2019-11-05 02:43:16
【问题描述】:

在 64 位机器上,我们知道地址是 8 个字节。 但是,我并不完全清楚一个地址中有多少字节的信息。 虚拟内存中的每个字节都有地址吗?还是内存中的每 64 位都有一个地址? 还是取决于架构?如果它取决于架构,那么我应该如何找到?

【问题讨论】:

  • 一个字节通常有一个地址,尽管有些架构要求内存访问为aligned。这个previous question 可能很有趣。您可以通过阅读其数据表了解特定处理器的要求。
  • 有例外,但大多数系统内存是字节可寻址的,一个字节是 8 位(有例外)。所以通常一个地址定义一个字节。但是您可以使用从该字节开始的寻址模式,并且可以包含多个字节。您可以从该地址开始进行 16 位传输,它将访问从该地址开始的两个字节,一个是地址,另一个是地址......等等。
  • 如何找到是阅读该架构和/或芯片的文档。

标签: assembly memory


【解决方案1】:

您的问题与this one有关。

还是取决于架构?

是的。这取决于架构:

对于大多数 CPU,一个地址代表 8 位

内存中的每个字节都有一个单独的地址。

TMS320 DSP 是一个 CPU 示例,其中一个地址代表 16 位

这意味着内存中的每个 16 位字 (uint16) 都有一个单独的地址。

还有一些计算机(其中许多是历史计算机),其中每个地址代表 121314162436 位(甚至更多)...

(不幸的是,我不知道使用 64 位地址而不是每个地址使用 8 位的 CPU 的示例,但我很确定这样的 CPU 也存在。)

还有一些内存类型并非所有地址都存在。这可能如下所示:

能被4整除的地址代表32位的信息;不能使用其他地址 - 这意味着这些地址完全没有任何信息

所以地址的“平均值”是 8 位,但没有代表 8 位的地址。

通常您会在安装了两种不同类型内存的计算机中看到这种行为,其中一种类型允许 8 位和 32 位访问,而另一种类型只允许 32 位访问。

外围设备的内存通常就是这种情况——例如某些微控制器中以太网控制器的内存。

据我没记错,我见过一个用于 PC 的 PCI SCSI 控制器,它也表现出这种行为。将该 SCSI 控制器安装到您的 64 位计算机中,并且您的计算机包含一定范围的地址,其中 25% 的地址代表 32 位数据,而所有地址的 75% 根本不代表任何数据。

我还看到了由大学学生设计的 CPU,其中“商业原件”允许 8 位和 32 位访问内存,但学生的副本仅允许 32 位访问。在这种情况下,整个地址范围都显示了这种行为。

顺便说一句:

在 64 位机器上,我们知道一个地址是 8 个字节。

即使这样也不一定正确:

据我所知,x86-64 CPU 仅使用 48 位地址。因此,编译器制造商可以将每个地址仅存储在 6 个字节的内存中。

当然,嵌入式设备的 CPU 内核可以设计为使用 x86-64 指令集的子集,但通常保存地址的寄存器(例如 rsp)只有 48 位宽。

【讨论】:

  • x86-64 规范地址是 48 位 符号 - 扩展至 64。未来一两代 CPU 预计将支持另外 9 个级别的页表虚拟地址空间位(57 位虚拟地址);英特尔已经发布了该扩展的规范。但无论如何,要使用规范范围高半部分的地址,寄存器的高部分必须有非零位。
  • 一个非正交 CPU 的一些寄存器更窄听起来非常不可信,并且会与使用标准 x86-64 System V 调用约定的工具链严重中断,该约定传递第二个整数/指针 arg相对强弱指数。 x86(-64) 唯一的好处之一是软件兼容性(以及现有实现的高性能,但是没有寄存器重命名的从头开始的重新设计不会有这个。)如果shr rsi, 63 将寄存器归零而不是将底部的标志位,那是行不通的。使用 64 位整数的软件也会被搞砸。
  • 如果从几个 reg 中保存 16 位是值得的,那么 x86-64 对您的微控制器来说是错误的 ISA 选择!!!一方面,它需要启用分页,因此您需要一个 TLB。这完全不可信,没有人会为了在几个寄存器中获得如此微小的收益而无缘无故地破坏 x86-64 ISA 兼容性。将这些 reg 的高 16 位硬连线为零将在设计中节省一些 SRAM 单元而无需重命名寄存器,但与解码 x86-64 指令(REX 前缀等)相比,这是一个巨大的变化
  • @PeterCordes 1) 我不小心输入了rsi 而不是rsp。 2)考虑一个链表,每个元素有 16 位数据和 2^30 个元素。使用 64 位地址和正确对齐保存此列表将需要 16 GB 的 RAM;使用 48 位地址只需要 8 GB 的 RAM。 x86-32 将无法处理此问题。此示例表明,可能存在每个地址存储 6 个字节可能有用的用例。
  • 当然,将数据打包到指针的高位可以节省空间;标记指针是一个众所周知的想法。但我不认为您可以将除 RSP 以外的任何寄存器的高位硬连接为零或符号扩展,而且您通常不会使用 RSP 进行指针追踪。你仍然需要shl rax,16 / sar rax,16 来重做符号扩展,或者如果你不使用高半部分,那么可能只是一个 AND 或 BMI1 ANDN 来复制和 - 和(所以你可以保存一个 @987654329 @ 用于通过移位提取数据。)
【解决方案2】:

在 C 中,char 是最小的可寻址单元。 (通常映射到最小的 asm 可寻址单元,但不能保证1。)limits.h 中的CHAR_BIT 告诉您它有多少位。这通常是8,但从历史上看,存在非 2 字节和字大小的机器。

在 asm 中,大多数现代 ISA 都是字节可寻址的,但现代 DSP 有时是字可寻址的,因为它们关心运行的唯一代码是处理整数或浮点数据的代码,而不是字符串。

如果它取决于架构,那我应该如何找到?

这是您从 ISA 手册中找到的内容,以及寄存器名称及其宽度等其他基础知识。这是机器语言的固有属性。


脚注 1:

您可以想象一个奇怪的假设 ISA,它是字节可寻址的,但字节存储可能被实现为包含(32 位)字的 非原子 读-修改-写。无论是在硬件中(非常不寻常)还是在软件中(实际上已经发生了)。

例如,DEC Alpha AXP 的前几个版本(90 年代为高性能和 64 位而设计的 RISC ISA)具有字节可寻址内存,但最窄的加载/存储是 32 位。

所以每个字节都有自己的地址,但是没有硬件指令可以修改单个字节。如果当时的 C 实现使用CHAR_BIT=8,他们将不得不用软件加载/合并/存储来模拟char 分配,除非他们知道这是一个带有填充(如本地或全局)的char 对象可能只是覆盖填充。

现代 C11 实现必须使用 CHAR_BIT=32 或使用慢速 LL/SC 重试循环在将 char* 取消引用为左值时自动替换字节,因为 C11 引入了不允许发明的内存模型在 C 抽象机中不会发生的写入(如读取并稍后重写相同的数据)。这可能会破坏单独线程写入相邻数据的代码。请参阅C++ memory model and race conditions on char arrays(C11 内存模型在这方面与 C++11 匹配)。

请参阅Can modern x86 hardware not store a single byte to memory? 了解更多详细信息,而不仅仅是 x86。

后来的 Alpha 模型引入了字节和半字加载/存储指令,因此编译器可以使用这些指令来实现具有单字节 char 的有效 C11 实现。

【讨论】:

  • 根据定义,在 C 中,char始终是最小的可寻址单元。
  • @prl:我的意思是 C char 可能不会映射到最小的 asm 可寻址单元。查看我的更新。
  • 啊,是的,我发现您的原始陈述以这种方式解释是有道理的。
【解决方案3】:

Peter &ch[1] 不起作用,但 ch 是一个字符指针,因此您可以继续添加 +1 以获取数组中的下一个字符。 证明:

cout<<sizeof(char)<<endl;
char ch[3]={55,33,70};
char *chp1=ch;
cout<<chp1<<" "<<&(chp1)<<" ";
char *chp2=ch+1;
cout<<chp2<<" "<<&(chp2)<<" ";
char *chp3=ch+2;
cout<<chp3<<" "<<&(chp3)<<endl;
return 0;

12 月 55 日是 7 字符 33是! 70 是 F https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html 它们被计算为 7!F, !F, F 因为字符数组被计算为空白。所以你可以看到指针指向上面数组的下一个字符。输出:i.stack.imgur.com/pBot5.jpg

cout<<sizeof(char)<<endl;
char ch[3]={55,33,70};
char *chp1=ch;
cout <<ch[0]<<" "<< (void*)(&ch[0])<<" "<<ch[1]<<" "<<(void*)(&ch[1])<<" "<<ch[2]<<" "<<(void*)(&ch[2])<<endl;

【讨论】:

  • &amp;ch[1] 具有正确的值,但您需要将其强制转换为 uintptr_tvoid* 以打印该值,而不是获得一个打印从该值开始的 C 字符串的 cout &lt;&lt; 重载地址。另外,编辑您以前的答案以修复它,而不是发布新答案。正如您在 Godbolt 上看到的(使用其在编译后运行程序的新功能)godbolt.org/z/m6DfAm 您的代码生成相隔 8 个字节的输出。这些是局部变量的地址,而不是数组元素。 x86 上的 C++ 有 1 字节 char。使用cout &lt;&lt; (void*)(&amp;ch[1])获取数组元素地址。
  • 我看到你的观点,指针指向彼此相距 1 个字节,但我不明白如何:“这些是局部变量的地址,而不是数组元素”这些元素给出了正确的值。
  • 您的原始代码打印为7!F 0x7ffe3cb239f0 !F 0x7ffe3cb239e8 F 0x7ffe3cb239e0。请注意您从 &amp;(chp1) 获得的地址的最后一位 080 - 获取指针局部变量的地址。它们相隔 8 个字节,因为 sizeof(char*) = 8 在我正在为其编译的 x86-64 系统上,编译器选择将 char*chp1 放在 char *chp2 旁边,依此类推。 godbolt.org/z/N9SihP 展示了如何实际打印数组元素的地址,例如0x7ffc37079524 0x7ffc37079525 0x7ffc37079526 0x7ffc37079527
  • 顺便说一句,cout&lt;&lt;chp1 并不安全,因为您没有0-终止您的char 数组。您很幸运,堆栈内存中有一个0,它恰好终止了ostream::operator&lt;&lt;(const char*) 将其视为的隐式长度C 字符串。否则它将继续运行并可能打印一堆额外的垃圾字节。
  • 啊哈,所以:char *chp2=ch+1; cout
【解决方案4】:

您可以通过更改变量来检查地址如何变化。

cout<<sizeof(char)<<endl;
char ch[3];
cout<<ch[0]<<" "<<std::hex<< (long)(char*)(&ch[0])<<" "<<ch[1]<<" "<<(long)(char*)(&ch[1])<<" "<<ch[2]<<" "<<(long)(char*)(&ch[2])<<endl;

【讨论】:

  • sizeof(char) 固定为1。但是CHAR_BIT 可能会有所不同。这将在 x86 与字可寻址 DSP 上打印相同的内容。它会检测到像 DEC Alpha 上的现代 C11 这样的情况,您可能有 32 位 char,因为机器缺少字节加载/存储,即使它是字节可寻址的。
  • 除非您的程序有问题,因为您打印的是单独的本地人的地址,而不是 &amp;ch[0]&amp;ch[1]
  • 当您编辑以修复答案时,您应该删除错误版本,而不是仅在底部添加更正。这不是论坛,只是提供正确的版本而不是编辑历史。顺便说一句,转换为 void* 将是让 iostream 打印指针值的更简单方法。 long 甚至不足以容纳 Windows x64 上的指针(其类型大小使用 LLP64 模型)。无论如何,这仍然不是 this 问题的好答案,因为您没有说明 C++ char 与 asm 中的一个字节之间的联系。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-12
  • 2019-04-22
  • 2010-09-11
  • 2016-03-23
  • 1970-01-01
相关资源
最近更新 更多