2

I have two console applications A1.exe A2.exe and one DLL. Both run in debug mode, optmization turned off. There is global const char* variable which I export from this dll and import back in A1 and A2:

//dll.h
extern "C" {DLLEXPORT extern const char* str;}
//dll.cpp
const char *str = "qwerty123";

I expect "qwerty123" to be created in read-only section of DLL and I expect that memory manager of Windows will map real memory with this string to some virtual memory address of A1.exe and different virtual address of A2.exe and do not create real copy of data. I expect that to happen also for all function definitions from that dll.

I run both applications at the same time and they both print correct strings imported from DLL. However I want some proof so I brutally use Cheat Engine to attach to A1.exe process and change that read-only string to some different value. Result is that A1.exe prints new value and A2.exe still prints old value. How to explain this? 1. I thought it is read-only memory and it will be shared to save real memory so why value changed only for one application? 2. How can I get proof that sections with program code (exported functions) are not duplicated for both processes?

user7242858
  • 81
  • 1
  • 9
  • 1
    only in case your data was in [/section](https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-6.0/aa278552(v=vs.60)) with `S` (Shared) attribute changes was global visible in all processes. otherwise, when process make some changes in dll image - it is private, visible only in this process and not affect other processes. really, if some global variable changed in dll, or code hook is set - this must not affect other processes. technically when you first time change some dll page - windows allocate private page copy for your process – RbMm Apr 02 '18 at 17:30

3 Answers3

2

The concept of 'read-only memory' is largely inapplicable to this discussion. Yes, the operating system has ways of making certain areas of memory appear as read-only to your program, but a) this memory is perfectly real just as any other memory, and b) your string literal is not being stored into any such memory anyway.

When you declare a string as const you are only preventing yourself from changing it from within your C++ program, and you might also be enabling some compiler optimizations, though as you have correctly turned them off while troubleshooting, they do not play any role in this discussion. To prove what I am talking about, you can cast-away the constness of that char* pointer, and then you can perfectly well modify the string pointed by it.

Each instance of the DLL has its very own data segment. DLL instances do not share their data. So, of course when you modify the item, you are only modifying it in the data segment of one instance of the DLL, while the data segment of the other DLL instance remains unchanged.

In order to share data you would need to use VirtualAllocEx() and WriteProcessMemory(), or better yet CreateFileMapping() and MapViewOfFile[Ex](), or some other mechanism.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • So where is that memory saving benefit? I see that many aricles say about that: https://msdn.microsoft.com/en-us/library/dtba4t8b.aspx Is it about loading dlls in runtme only? I thought that text sections of program are shared. – user7242858 Apr 02 '18 at 17:19
  • For dynamically sharing memory between processes, use `CreateFileMapping` to create a shareable Section object backed by the paging file, not `GlobalAlloc` (current process only). Or use a shared data section (e.g. see [`data_seg`](https://learn.microsoft.com/en-us/cpp/preprocessor/const-seg)) and the linker [/section](https://learn.microsoft.com/en-us/cpp/build/reference/section-specify-section-attributes) option to change its attributes. – Eryk Sun Apr 02 '18 at 18:57
  • Outdated info. Compilers nowadays are actually quite good at distinguishing between .data and .rdata sections. And .rdata is set to read only access. So no you can't really cast away constness. – rustyx Apr 02 '18 at 18:58
  • @user7242858 the operating system will store the code of both instances of the DLL in the same area of physical memory. "text" is (an awful misnomer for) the code segment. So, the savings are in the code, not in the data. – Mike Nakis Apr 02 '18 at 19:19
  • @rustyx so how do you explain the fact that the OP managed to change the memory using the debugger? – Mike Nakis Apr 02 '18 at 19:20
  • There are [historical reasons](https://stackoverflow.com/questions/3075049/why-do-compilers-allow-string-literals-not-to-be-const) for allowing modification of string literals. Just `const int x = 1;` would have been a better test. – rustyx Apr 02 '18 at 19:30
  • Please fix the last paragraph of this answer to correctly address the OP's example, with one DLL loaded in two executables. This cannot be addressed by passing data between function calls or using `GlobalAlloc`. It requires shared memory or some other form of IPC (e.g. sockets, named pipes). – Eryk Sun Apr 02 '18 at 20:45
  • @eryksun wh00ps, I thought I remembered what `GlobalAlloc` does, but I was wrong. – Mike Nakis Apr 02 '18 at 20:51
  • 1
    @MikeNakis: back in 16bit Windows, `GlobalAlloc()` would indeed have allocated global sharable memory, but [that is no longer true since 32bit Windows came along](https://msdn.microsoft.com/en-us/library/windows/desktop/aa366596.aspx). – Remy Lebeau Apr 02 '18 at 20:54
  • `VirtualAllocEx()` and `WriteProcessMemory()` is not an efficient way to share memory. That requires the Memory Manager to attach to both processes, map the source and target into system virtual memory and copy data from one process to another -- which above all else assumes one process has access to write to the memory of another. Instead, this should be a shared, securable Section created via `CreateFileMapping` and mapped via `MapViewOfFile[Ex]`. This points the page-table entries of both processes to a common set of prototype page-table entries. – Eryk Sun Apr 02 '18 at 20:58
2

The optimization you are thinking about here is called copy on write. In the case of Visual C++ all global variables defined in a DLL are initially loaded in shared memory pages with the PAGE_WRITECOPY attribute. If some process writes into such a location it receives its own page with PAGE_READWRITE attribute and uses it further on. Visual C++ seems to make no difference between const and non const global variables as that property is a compiler feature. For instance it can be thrown away with a cast and handling it from the OS perspective would be a headache and a security hole as well.

Try searching web for PAGE_WRITECOPY to learn more details.

jszpilewski
  • 1,632
  • 1
  • 21
  • 19
  • Section mappings have attributes to determine read, write, and execute access and whether the memory is shared. This is applied subsequent to the DLL's initial execute-write-copy allocation mapping. String literal constants may or may not be stored in the read-write .data section. Numeric and pointer constants (e.g. `char * const p`, i.e. the pointer value itself, not what it references) should be stored in the read-only .rdata section. – Eryk Sun Apr 02 '18 at 22:23
  • The question owner's example show that the global const variable 'str' is not in a memory page with `PAGE_WRITECOPY` attribute. Because he brutally use Cheat Engine to attach to A1.exe process and change that read-only string to some different value. – ligand Nov 27 '19 at 09:47
0

The value of a 'read-only global variable' of a DLL may be different for processes which attach the DLL. Thus, it is unsafe if the DLL shares the single physical memory of a 'read-only global variable' for several processes. The cautious but safe solution is to create differenct physical memory instances of a 'read-only global variable' for several processes.

For example, in the dll.cpp,

int foo(){return 1;}
typedef int (*C_pFunc)();
const C_pFunc pf=foo;

Obviously, A.exe and B.exe both load the dll, function foo() in the DLL may have different logical addresses in A.exe and B.exe. Therefore, the global const variable pf may have different initialized values in A.exe and B.exe.

This is a typical phenomenan for COM dll, when the COM dll is attached to a process, many vtables as global const variables should be initialized with the function logical addresses in this process.

ligand
  • 182
  • 1
  • 5