2

On a 64bit machine, we know that an address is 8 bytes. However, I am not entirely clear how many bytes of information is in one address. Does every byte in virtual memory have an address? Or does every 64 bits in memory have an address? Or does it depend on the architecture? If it depends on the architecture, then how should I find out?

user1559897
  • 1,454
  • 2
  • 14
  • 27
  • One byte usually has one address although some architectures require memory access to be [aligned](https://en.wikipedia.org/wiki/Data_structure_alignment). This [previous question](https://stackoverflow.com/questions/1063809/aligned-and-unaligned-memory-accesses) may be of interest. You find out what is required by a particular processor by reading its data sheet. – Weather Vane Jun 22 '19 at 16:56
  • there are exceptions but most systems memory is byte addressable and a byte is 8 bits (there are exceptions). So in general one address defines a byte. But you can have addressing modes that START at that byte and can contain more than one byte. You can do a 16 bit transfer starting at that address and it will access two bytes starting at that address, the one being addresses and the one next to it....and so on. – old_timer Jun 22 '19 at 23:41
  • how you find out is read the documentation for that architecture and/or chip. – old_timer Jun 22 '19 at 23:42

4 Answers4

5

Your question is related to this one.

Or does it depend on the architecture?

Yes. It depends on the architecture:

For most CPUs one address represents 8 bits.

Each byte in the memory has an individual address.

The TMS320 DSP is an example for a CPU where one address represents 16 bits.

This means that each 16-bit word (uint16) in the memory has an individual address.

There are also computers (many of them historic) where each address represents 12, 13, 14, 16, 24 or 36 bits (and maybe even more) ...

(Unfortunately, I don't know an example for a CPU using 64-bit addresses not using 8 bits per address but I'm quite sure such CPUs also exist.)

Then there are memory types where not all addresses exist. This may look the following way:

Addresses which are divisible by 4 represent 32 bits of information; other addresses cannot be used - which means that these addresses represent no information at all.

So the "average" over the addresses is 8 bits but there is no address that represents 8 bits.

Typically you see such a behavior in a computer where two different types of memory are installed and one type allows both 8- and 32-bit access while the other type only allows 32-bit access.

And this is often the case for the memory of peripheral devices - for example the memory of the Ethernet controller in some microcontrollers.

Is far as I remember correctly, I have seen a PCI SCSI controller for PCs that also showed this behavior. Install that SCSI controller into your 64-bit computer and your computer contains some range of addresses where 25% of all addresses represent 32 bits of data and 75% of all addresses don't represent any data at all.

I've also seen CPUs designed by students of universities where the "commercial original" allows 8- and 32-bit access to the memory but the student's replica allows only 32-bit access. In this case the whole address range shows this behavior.

By the way:

On a 64bit machine, we know that an address is 8 bytes.

Even this is not necessarily true:

As far as I know, an x86-64 CPU uses only 48-bit addresses. Therefore it would be possible for a compiler manufacturer to store each address in only 6 bytes of memory.

And of course CPU cores for embedded devices could be designed to use a subset of the x86-64 instruction set but registers that typically hold an address (such as rsp) are only 48 bits wide.

Martin Rosenau
  • 17,897
  • 3
  • 19
  • 38
  • x86-64 canonical addresses are 48-bit *sign*-extended to 64. Future CPUs in the next generation or two are expected to support another level of page tables for another 9 bits of virtual address space (57-bit virtual addresses); Intel has already published specs for the extension. But anyway, to use an address in the high half of the canonical range, you must have non-zero bits in the high part of the register. – Peter Cordes Jun 23 '19 at 06:02
  • A non-orthogonal CPU with some of the registers being narrower sounds highly implausible, and would horribly break with toolchains that use the standard x86-64 System V calling convention that pass the 2nd integer/pointer arg in RSI. One of the only good things about x86(-64) is software compatibility (and high performance of existing implementations, but a ground-up redesign without register renaming won't have that.) If `shr rsi, 63` zeroes the register instead of putting the sign bit at the bottom, that's just not going to work. Software that uses 64-bit integers would also be screwed. – Peter Cordes Jun 23 '19 at 06:07
  • If saving 16 bits from a couple regs is worth it, x86-64 is the wrong ISA choice for your microcontroller!!! It requires paging to be enabled, for one thing, so you need a TLB. It's just completely implausble, and nobody would gratuitously break x86-64 ISA compatibility for such a tiny gain in a couple registers. Hard-wiring the high 16 bits of those regs to zero would save a few SRAM cells in a design without register renaming, but that's total chump change compared to decoding x86-64 instructions (REX prefixes, etc.) – Peter Cordes Jun 23 '19 at 06:10
  • @PeterCordes 1) I accidentally typed `rsi` instead of `rsp`. 2) Think of a linked list with 16 bits of data per element and 2^30 elements. Saving this list using 64-bit addresses and a correct alignment would require 16 Gigabytes of RAM; using 48-bit addresses would require only 8 Gigabytes of RAM. x86-32 would not be able to handle this. This example shows that there might be use cases where storing 6 bytes per address might be useful. – Martin Rosenau Jun 23 '19 at 08:01
  • Sure, packing data into the high bits of pointers can save space; tagged pointers are a well-known idea. But I don't think you can plausibly hard-wire the high bits of any registers other than RSP to zero or sign-extension, and you don't normally use RSP for pointer-chasing. You'll still need `shl rax,16` / `sar rax,16` to redo sign-extension, or if you don't use the high half then maybe just an AND or BMI1 ANDN to copy-and-AND (so you can save a `mov` for extracting the data with a shift.) – Peter Cordes Jun 23 '19 at 13:20
  • What you *could* do is build an x86-64 that didn't enforce canonical addresses, and silently ignored high address bits instead of faulting if an effective address wasn't correctly sign-extended from 48 to 64. That makes massively more sense than hard-wiring any registers, and saves instructions for pointer chasing your linked list or tree. – Peter Cordes Jun 23 '19 at 13:22
4

In C, char is the smallest addressable unit. (And usually maps to the smallest asm addressable unit, but there's no guarantee of that1.) CHAR_BIT from limits.h tells you how many bits it has. That's usually 8 but historically there have been machines with non-power-of-2 byte and word sizes.

In asm, most modern ISAs are byte-addressable, but modern DSPs are sometimes word-addressable because the only code they care about running is code that processes integer or float data, not strings.

If it depends on the architecture, then how should I find out?

This is something you find out from the ISA manual, along with other basics like names of registers and their widths. It's an inherent property of the machine language.


Footnote 1:

You could image a weird hypothetical ISA that was byte addressable, but where a byte store might be implemented as a non-atomic read-modify-write of the containing (32-bit) word. Either in hardware (extremely unusual) or in software (which has actually happened).

For example, the first couple versions of DEC Alpha AXP (a RISC ISA from the 90s aggressively designed for high performance and 64-bit) had byte addressable memory, but the narrowest load/store was 32-bit.

So every byte had its own address, but there were no hardware instructions to modify a single byte. If C implementations at the time used CHAR_BIT=8, they would have to emulate char assignment with a software load / merge / store, unless they knew it was a char object with padding (like a local or global) where they could just overwrite the padding.

A modern C11 implementation would have to use CHAR_BIT=32 or use a slow LL/SC retry loop to atomically replace a byte when dereferencing a char* as an lvalue, because C11 introduced a memory model that doesn't allow inventing writes (like read and later rewrite the same data) that don't happen in the C abstract machine. That could break code where separate threads write adjacent data. See C++ memory model and race conditions on char arrays (the C11 memory model matches C++11 in this regard).

See also Can modern x86 hardware not store a single byte to memory? for more details, not just x86.

Later models of Alpha introduced byte and halfword load/store instructions, so compilers could use those to implement a valid C11 implementation with one-byte char.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
-2

You can check how addresses change by changing variable.

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;
Gen0me
  • 115
  • 8
  • `sizeof(char)` is fixed at `1`. But `CHAR_BIT` can vary. This will print the same thing on x86 vs. a word-addressable DSP. It would detect cases like moder C11 on DEC Alpha where you might have 32-bit `char` because the machine lacks byte load/store, even though it is byte addressable. – Peter Cordes Jun 22 '19 at 20:09
  • Except your program is buggy because you print the address of the separate locals, not `&ch[0]` and `&ch[1]`. – Peter Cordes Jun 22 '19 at 20:10
  • when you edit to fix an answer, you should remove the buggy version instead of just adding a correction at the bottom. This isn't a forum, just present the correct version not an edit history. And BTW, casting to `void*` would be an easier way to get iostream to print the pointer value. `long` isn't even big enough to hold a pointer on Windows x64 (its type sizes use an LLP64 model). Anyway, this still isn't a good answer to *this* question because you don't say anything about the connection between C++ `char` vs. a byte in asm. – Peter Cordes Jun 23 '19 at 15:01
-2

Peter &ch[1] doesnt work, but ch is a char pointer so you can move on it adding +1 to get next char in array. As the proove:

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;

Dec 55 is 7 in char 33 is ! 70 is F https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html They are being couted as 7!F, !F, F because arrays of chars are couted up to blank space. So as you can see pointers point to next chars of above array. Output: 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;
Gen0me
  • 115
  • 8
  • `&ch[1]` has the right value, but you need to cast it to `uintptr_t` or `void*` to print the value instead of getting a `cout <<` overload that prints a C string starting at that address. Also, edit your previous answer to fix it instead of posting new ones. As you can see on Godbolt (using its new feature that runs the program after compiling it) https://godbolt.org/z/m6DfAm your code produces outputs that are 8 bytes apart. Those are the addresses of the local variables, not the array elements. C++ on x86 has 1-byte `char`. Use `cout << (void*)(&ch[1])` to get the array element address. – Peter Cordes Jun 22 '19 at 21:43
  • I see your point with pointers pointing 1 byte distant from each other, but I dont see how: "Those are the addresses of the local variables, not the array elements" Those elements give correct values. – Gen0me Jun 22 '19 at 22:10
  • Your original code prints `7!F 0x7ffe3cb239f0 !F 0x7ffe3cb239e8 F 0x7ffe3cb239e0`. Note the `0`, `8`, `0` last digits of the addresses you get from **`&(chp1)` - taking the address of a pointer local variable**. Those are 8 bytes apart, because `sizeof(char*)` = 8 on the x86-64 system I'm compiling for, and the compiler chose to put `char*chp1` next to `char *chp2` and so on. https://godbolt.org/z/N9SihP shows how to actually print the addresses of the array elements, `0x7ffc37079524 0x7ffc37079525 0x7ffc37079526 0x7ffc37079527` for example. – Peter Cordes Jun 22 '19 at 22:23
  • And BTW, `cout< – Peter Cordes Jun 22 '19 at 22:25
  • Aha so: char *chp2=ch+1; cout<<&chp2; couts address of the pointer to pointer ch[1]. Than why chp2 couts variable instead of pointer address? Why casting (void *)ch[1] gives address to the ch1, but (char *)ch[1] gives ch[1]? – Gen0me Jun 22 '19 at 22:49
  • Not `(void *)ch[1]`. I said `(void *)&ch[1]`. Using your tmp var, that's like `(void *)chp1`. The difference is that `ostream::operator<<` has an overload for `char*` that treats it as an implicit-length C string. But for `void*` it prints the address as hex. This is simpler if you use `printf`: `printf("%p\n", chp1)` prints the pointer value. `printf("%s\n", chp1);` dereferences the pointer to get at the pointed-to C string. `ostream` overloads are inconvenient when you want to treat `char*` like any other pointer or `unsigned char` / `uint8_t` as a number, not a character. – Peter Cordes Jun 22 '19 at 23:02
  • I see. cout < – Gen0me Jun 22 '19 at 23:36