|
Managed. In my illness induced stupor, I thought we were in the C# forum.
|
|
|
|
|
The typical way for functions to work is allocating whatever stack space they need once in the prologue. But there's no need for anint to be in memory here. Or to exist at all for that matter, since the loop trivially doesn't do anything and can be skipped. MSVC compiles this function like this, anint isn't anywhere, not in memory, not in a register, just gone.
If it was going to be in memory, then only one instance of it needs to exist, so that's what happens. Or should happen anyway. If a compiler individually allocated separate copies of that variable for each iteration, I would file a bug report.
|
|
|
|
|
|
By the way in general it is not right to think of variables as being allocated anywhere, neither in memory nor in registers. Variable are not the "thing" that is allocated, and any given variable may end being in zero or more places at the same time, if you insist on looking at it like that. It's not a completely useless mental model, which is probably why it persists, but that's as a lie-to-children. If a variable is assigned to various times (in the static sense: not so much several times in a loop, but several times in straight line code), those different "versions" of the variable may well end up in different places. SSA considers those different "versions" of the variable to be different variables altogether. Furthermore, even one "version" of a variable can be split into multiple live ranges - that's not just theoretical, there can be multiple good reasons to split it and allocated the pieces to different places. For example, there are often restrictions on which set of register can be used for some instructions, such as on x64 divisions and "legacy" shift-by-variable instructions.
For example, if we consider this code with a division and shift-by-variable:
int test(int x, int y)
{
x = x / y;
return y << x;
}
[MSVC compiles it like this, for x64](https://godbolt.org/z/T8dGWMzhc) (why doesn't this link linkify?)
0 x$ = 8
1 y$ = 16
2 int test(int,int) PROC
3 mov r8d, edx
4 mov eax, ecx
5 cdq
6 idiv r8d
7 mov ecx, eax
8 shl r8d, cl
9 mov eax, r8d
10 ret 0
11 int test(int,int) ENDP
On lines 0 and 1 MSVC helpfully defined stack offsets for x and y, which aren't used, they never end up being on the stack. x is passed in via ecx , and y via edx .
x begins in ecx , then is copied to eax (line 4) because idiv takes the dividend in edx:eax , the division leaves it in eax (only because the code happens to assign the result of x / y back to x - to be clear, the output would be in eax either way, but eax could have represented some other variable otherwise), the original un-divided value of x is still in ecx at this point (after the division on line 6 but before the mov on line 7) but we need the new value to be in ecx , because shl needs the shift count to be in cl which is the lowest byte of ecx . Clearly if we ask "where is x ", it depends on which line of the assembly code (not even the C++ source code) we ask that question about.
y begins in edx , but it cannot stay there because idiv uses edx as input for the upper half of the dividend, and as output for the remainder, so y is copied to r8d , and it stays there. The result of y << x is copied into eax (the return value needs to be in eax ) but that's not really y itself. I could have written y <<= x; return y; and then the same assembly code results, but then eax does represent y .
Let's turn things up a notch. I wrote that a variable may be in multiple places, let's see it:
#include <stddef.h>
int test(size_t N, int *data)
{
int sum = 0;
for (size_t i = 0; i < N; i++)
sum += data[i];
return sum;
}
Compiler Explorer
N$ = 8
data$ = 16
int test(unsigned __int64,int * __ptr64) PROC
xor r8d, r8d
mov r11, rcx
mov r10d, r8d
mov eax, r8d
cmp rcx, 8
jb SHORT $LN9@test
xorps xmm2, xmm2
and rcx, -8
movdqa xmm1, xmm2
npad 3
$LL4@test:
movdqu xmm0, XMMWORD PTR [rdx+rax*4]
paddd xmm0, xmm2
movdqa xmm2, xmm0
movdqu xmm0, XMMWORD PTR [rdx+rax*4+16]
add rax, 8
paddd xmm0, xmm1
movdqa xmm1, xmm0
cmp rax, rcx
jb SHORT $LL4@test
paddd xmm1, xmm2
movdqa xmm0, xmm1
psrldq xmm0, 8
paddd xmm1, xmm0
movdqa xmm0, xmm1
psrldq xmm0, 4
paddd xmm1, xmm0
movd r10d, xmm1
$LN9@test:
mov r9d, r8d
cmp rax, r11
jae SHORT $LN20@test
mov rcx, r11
sub rcx, rax
cmp rcx, 2
jb SHORT $LC14@test
lea rcx, QWORD PTR [r11-1]
npad 1
$LL16@test:
add r8d, DWORD PTR [rdx+rax*4]
add r9d, DWORD PTR [rdx+rax*4+4]
add rax, 2
cmp rax, rcx
jb SHORT $LL16@test
$LC14@test:
cmp rax, r11
jae SHORT $LN15@test
add r10d, DWORD PTR [rdx+rax*4]
$LN15@test:
lea eax, DWORD PTR [r9+r8]
add eax, r10d
ret 0
$LN20@test:
mov eax, r10d
ret 0
int test(unsigned __int64,int * __ptr64) ENDP
Lots of stuff going on here, but here's the important part: there are 4 sums, held in one vector register. xmm2 usually holds those sums. After paddd xmm0, xmm2 it's really xmm0 that holds the sums, then movdqa xmm2, xmm0 immediately copies them back to xmm2 though. And by the way, yes I think that's a mildly silly way to do it, MSVC could have used paddd xmm2, XMMWORD PTR [rdx+rax*4] instead of that movdqu \ paddd \ movdqa sequence, and while "number of instructions" is a poor metric I do believe that that would just be a better way to do it. Especially on CPUs that do not have move-elimination. But whatever, MSVC does what it does.
After the label $LL16@test there is a small unrolled-by-a-factor-of-2 loop where both r8d and r9d are used to calculate more sums, but are they sum ? However you look at it, r8d and r9d are used to calculate part of the sum. r10d also holds part of the sum at this point, namely the part that was calculated by the vectorized loop .. also up to one extra element may be summed into r10d , if one element is left over (ie if N is odd). After $LN15@test , a lea and add are used to add up all 3 parts of the sum that exist at that point.
modified 6-Oct-23 7:05am.
|
|
|
|
|
I'm using the following function for years and used it under any Version of Windows since XP. It creates a systemtask with the start type set to "auto", just like it is expected to do. But under windows 11, the start type of the installed service always defaults to "manual" when being created. Any help solving this is very much appreciated. Am using VS2022 using toolset 1.41_XP (for reasons)
(already added some extra check for win 11... but still the start type defaults to manual in the newly created task)
static int manage_service(int action) {
SC_HANDLE hSCM = NULL, hService = NULL;
SERVICE_DESCRIPTION descr = { server_name };
char path[PATH_MAX + 20];
int success = 1;
GetModuleFileName(NULL, path, sizeof(path));
strncat(path, " ", sizeof(path));
strncat(path, service_magic_argument, sizeof(path));
if (IsRunAsAdministrator()) {
if ((hSCM = OpenSCManager(NULL, NULL, action == ID_INSTALL_SERVICE ?
GENERIC_WRITE : GENERIC_READ)) == NULL) {
success = 0;
show_error();
}
else if (action == ID_INSTALL_SERVICE) {
hService = CreateService(hSCM, service_name, service_name,
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
path, NULL, NULL, NULL, NULL, NULL);
if (hService) {
ChangeServiceConfig(hService, SERVICE_NO_CHANGE, SERVICE_AUTO_START,
SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &descr);
OSVERSIONINFOEX osvi;
ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
osvi.dwMajorVersion = 11;
if (GetVersionEx((OSVERSIONINFO*)&osvi)) {
ChangeServiceConfig(hService, SERVICE_NO_CHANGE, SERVICE_AUTO_START,
SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
}
}
else {
show_error();
}
}
else if (action == ID_REMOVE_SERVICE) {
if ((hService = OpenService(hSCM, service_name, DELETE)) == NULL ||
!DeleteService(hService)) {
show_error();
}
}
else if ((hService = OpenService(hSCM, service_name,
SERVICE_QUERY_STATUS)) == NULL) {
success = 0;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
}
else {
if (action == ID_INSTALL_SERVICE) {
RunServiceAsAdmin('I', path, service_name);
}
else if (action == ID_REMOVE_SERVICE) {
RunServiceAsAdmin('R', path, service_name);
}
else {
if ((hSCM = OpenSCManager(NULL, NULL, GENERIC_READ)) == NULL) {
success = 0;
show_error();
}
if ((hService = OpenService(hSCM, service_name,
SERVICE_QUERY_STATUS)) == NULL) {
success = 0;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
}
}
return success;
}
|
|
|
|
|
Hmmm,
Your code looks good to me. Although it would be nice if you captured the return values of ChangeServiceConfig .
I would recommend debugging this by checking the Event logs. Look for event ID 7040 in the "Service Control Manager" log source. You might need to enable auditing.
Also, try temporarily adding a Windows Defender exclusion on the service file path if your executable is unsigned/untrusted. I'm wondering if Defender is blocking the change.
|
|
|
|
|
Just found out somethhing more.... As soon as I invoke the service creation function from within the program, I do get the normal service controll manager asking for elevated rights in order to create the service, what is exactly what happen. But then the service gets created with start type set to "manual".
If I do start the program manually "as Administrator" and then invoke the service creation function, the service gets created correctly with start type "auto". So there probably might be a problem with my elevation of rights!?... will check this. Strange though, that it works fin under any Windows version since XP... just not windows 11...
Here is the code to start with elevated rights:
BOOL IsRunAsAdministrator()
{
BOOL isRunAsAdmin = FALSE;
DWORD dwError = ERROR_SUCCESS;
PSID pAdministratorsGroup = NULL;
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
if (!AllocateAndInitializeSid(
&NtAuthority,
2,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&pAdministratorsGroup))
{
goto Cleanup;
}
if (!CheckTokenMembership(NULL, pAdministratorsGroup, &isRunAsAdmin))
{
goto Cleanup;
}
Cleanup:
if (pAdministratorsGroup)
{
FreeSid(pAdministratorsGroup);
pAdministratorsGroup = NULL;
}
return isRunAsAdmin;
}
void RunServiceAsAdmin(char ch, const char *program, const char* name)
{
char param[255];
SHELLEXECUTEINFO sei = { sizeof(sei) };
memset(param, 0 , sizeof(param));
sei.lpVerb = "runas";
sei.lpFile = "sc.exe";
sei.hwnd = NULL;
sei.nShow = SW_NORMAL;
if(ch == 'I')
{
sprintf(param, "create \"%s\" binPath= \"%s\" DisplayName=\"%s\"", name, program, name);
}
else
{
sprintf(param, "delete \"%s\"", name);
}
sei.lpParameters = param;
if (!ShellExecuteEx(&sei))
{
show_error();
}
}
modified 23-Sep-23 18:23pm.
|
|
|
|
|
Well,
You appear to have a function that is checking if you are running as Administrator. Could you show me the content of that function?
|
|
|
|
|
Here is some more complete code:
BOOL IsRunAsAdministrator()
{
BOOL isRunAsAdmin = FALSE;
DWORD dwError = ERROR_SUCCESS;
PSID pAdministratorsGroup = NULL;
SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
if (!AllocateAndInitializeSid(
&NtAuthority,
2,
SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&pAdministratorsGroup))
{
goto Cleanup;
}
if (!CheckTokenMembership(NULL, pAdministratorsGroup, &isRunAsAdmin))
{
goto Cleanup;
}
Cleanup:
if (pAdministratorsGroup)
{
FreeSid(pAdministratorsGroup);
pAdministratorsGroup = NULL;
}
return isRunAsAdmin;
}
void RunServiceAsAdmin(char ch, const char *program, const char* name)
{
char param[255];
SHELLEXECUTEINFO sei = { sizeof(sei) };
memset(param, 0 , sizeof(param));
sei.lpVerb = "runas";
sei.lpFile = "sc.exe";
sei.hwnd = NULL;
sei.nShow = SW_NORMAL;
if(ch == 'I')
{
sprintf(param, "create \"%s\" binPath= \"%s\" DisplayName=\"%s\"", name, program, name);
}
else
{
sprintf(param, "delete \"%s\"", name);
}
sei.lpParameters = param;
if (!ShellExecuteEx(&sei))
{
show_error();
}
}
static int manage_service(int action) {
SC_HANDLE hSCM = NULL, hService = NULL;
SERVICE_DESCRIPTION descr = { server_name };
char path[PATH_MAX + 20];
int success = 1;
GetModuleFileName(NULL, path, sizeof(path));
strncat(path, " ", sizeof(path));
strncat(path, service_magic_argument, sizeof(path));
if (IsRunAsAdministrator()) {
if ((hSCM = OpenSCManager(NULL, NULL, action == ID_INSTALL_SERVICE ?
GENERIC_WRITE : GENERIC_READ)) == NULL) {
success = 0;
show_error();
}
else if (action == ID_INSTALL_SERVICE) {
hService = CreateService(hSCM, service_name, service_name,
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
path, NULL, NULL, NULL, NULL, NULL);
if (hService) {
ChangeServiceConfig(hService, SERVICE_NO_CHANGE, SERVICE_AUTO_START,
SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &descr);
OSVERSIONINFOEX osvi;
ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
osvi.dwMajorVersion = 11;
if (GetVersionEx((OSVERSIONINFO*)&osvi)) {
ChangeServiceConfig(hService, SERVICE_NO_CHANGE, SERVICE_AUTO_START,
SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
}
}
else {
show_error();
}
}
else if (action == ID_REMOVE_SERVICE) {
if ((hService = OpenService(hSCM, service_name, DELETE)) == NULL ||
!DeleteService(hService)) {
show_error();
}
}
else if ((hService = OpenService(hSCM, service_name,
SERVICE_QUERY_STATUS)) == NULL) {
success = 0;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
}
else {
if (action == ID_INSTALL_SERVICE) {
RunServiceAsAdmin('I', path, service_name);
}
else if (action == ID_REMOVE_SERVICE) {
RunServiceAsAdmin('R', path, service_name);
}
else {
if ((hSCM = OpenSCManager(NULL, NULL, GENERIC_READ)) == NULL) {
success = 0;
show_error();
}
if ((hService = OpenService(hSCM, service_name,
SERVICE_QUERY_STATUS)) == NULL) {
success = 0;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
}
}
return success;
}
static LONG queryServiceStatus(const char* serviceName){
SC_HANDLE hSCM = NULL, hService = NULL;
SERVICE_STATUS_PROCESS ssStatus;
DWORD dwBytesNeeded;
LONG status = 0;
if ((hSCM = OpenSCManager(NULL, NULL, GENERIC_READ)) == NULL) {
show_error();
return 0;
}
hService = OpenService(
hSCM,
serviceName,
SERVICE_QUERY_STATUS);
if (hService == NULL)
{
goto END_QUERY;
}
if (!QueryServiceStatusEx(
hService,
SC_STATUS_PROCESS_INFO,
(LPBYTE) &ssStatus,
sizeof(SERVICE_STATUS_PROCESS),
&dwBytesNeeded ) )
{
goto END_QUERY;
}
if(ssStatus.dwCurrentState == SERVICE_RUNNING)
{
status = SERVICE_RUNNING;
goto END_QUERY;
}
modified 23-Sep-23 18:31pm.
|
|
|
|
|
I do see a bug.
SHELLEXECUTEINFO sei = { sizeof(sei) };
You should zero that struct out. Then set the cbSize member. Not sure if this is causing your problem though.
I'm on my TV right now so reviewing on my couch. But don't see any other issues.
|
|
|
|
|
Thanks for the tip!
Am also currently looking into a way to use the ControlService utility via CreateProcess to install the service, instead of calling CreateService directly... But not sure if I can pull that off correctly...
|
|
|
|
|
I just noticed in your original post:
Rick R. 2023 wrote:
Am using VS2022 using toolset 1.41_XP
Do you get the same behavior if you compile for Windows 11?
|
|
|
|
|
Starting to suspect the toolset, too.
Have to fix a bunch of linker problems caused by my old and pretty messy project settings, in order to test with a newer version... might take a while.
|
|
|
|
|
Well,
Alot of members check the forums everyday. Sometimes it just helps to have a few other eyes look at the issue.
Rick R. 2023 wrote: Have to fix a bunch of linker problems
XP to Win11 is a big jump, I can imagine.
|
|
|
|
|
how to create a fingerprint sensor code with c++ from scratch
|
|
|
|
|
Probably you mean 'how to create from scratch a C++ application interfacing a fingerprint sensor module (directly handling the fingerprint sensor would be far more difficult, I suppose).
If I got you then you should carefully read the documentation of the module and implement yourself the appropriate communication code.
You may also have a look at existing libraries source code (e.g. Arduino).
"In testa che avete, Signor di Ceprano?"
-- Rigoletto
|
|
|
|
|
Hi there, I can't understand why my char type array isn't displayed with printf using string specifier %s. Here is my code.
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SIZE 10
void findbinary(int number, char result[], int index);
int main (void)
{
int someNumber = 233;
char result[SIZE];
int index = 0;
size_t i;
findbinary(someNumber, result,0);
printf("Decimal %d in binary = %c ",someNumber);
for(i = 0; result[i] != '\0'; i++)
printf("%s",result[i]);
}
void findbinary(int number, char result[], int index)
{
if(number == 0){
return;
}
result[index] = number % 2;
findbinary(number / 2, result, index + 1);
}
It displayed properly if I use %d specifier, but... this is char array..?
Thank you.
modified 20-Sep-23 11:07am.
|
|
|
|
|
In:
printf("%s",result[i]) result[i] is a char not a string. It should be printed with '%c' not '%s'.
Also in:
printf("Decimal %d in binary = %c ",someNumber); you have 2 format specifiers (%d and %c) and only one argument. All hell can break loose when program accesses nonexistent argument.
Mircea
|
|
|
|
|
Your compiler will warn you about issues with printf formats not agreeing with the arguments provided. MS C seems to do this even without any additional warning flags. If you're using GCC (linux) or clang (Apple?) then you can add -Wall to the command line. The -Wall flag will generate warnings for the most often occurring code issues that are usually the cause of bugs. It's probably a good idea to add -Wextra to the command line, too.
Keep Calm and Carry On
|
|
|
|
|
Probably you meant something similar to
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SIZE 10
void findbinary(int number, char result[], int index);
int main (void)
{
int someNumber = 233;
char result[SIZE];
findbinary(someNumber, result,0);
printf("Decimal %d in reversed binary %s\n", someNumber, result);
return 0;
}
void findbinary(int number, char result[], int index)
{
if ( index == SIZE)
exit(-1);
if(number == 0){
result[index] = '\0'; return;
}
result[index] = (number % 2) + '0'; findbinary(number / 2, result, index + 1);
}
Note you are representing the binary number 'reversed' (that is leftmost bit is the least significant).
"In testa che avete, Signor di Ceprano?"
-- Rigoletto
|
|
|
|
|
I was trying to get the following to work:
#include <iostream>
#include <string>
#include <array>
std::string exec(const char* cmd) {
std::array<char, 128> buffer;
std::string result;
std::unique_ptr<FILE, decltype(&_pclose)> pipe(_popen(cmd, "r"), _pclose);
if (!pipe) {
throw std::runtime_error("popen() failed!");
}
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
result += buffer.data();
}
return result;
}
void main() {
std::cout << exec("wmic bootconfig get description");
std::cout << exec("wmic diskdrive where DeviceID='\\.\PHYSICALDRIVE3' get model,serialnumber");
}
The first exec is running fine and giving the right return, but I'm having problems with the second exec, where I'm getting: ERROR: Description = Invalid query. In my search for a fix for that error, I found that because I'm using C++ I should be using native wmi queries, but unfortunately each result with code examples that I found were "like 3 pages" of code. Does anyone know of some more simple examples for wmi queries?
As for the code, I'm trying to get the model and serial number of the disk that the OS is installed on for some unique identification of app install and some other minor checks. And for this, I found that using wmic bootconfig get description , you can parse that result (Description\Device\Harddisk3\Partition1 , 3 is the index) and get the index of the disk the OS is installed on, then using that index and wmic diskdrive where DeviceID='\\.\PHYSICALDRIVE3' get model,serialnumber where you change the 3 from PHYSICALDRIVE3 with the index from last command, you get the model and serial number. The code above is just a test to see the commands working, later I was going to parse the result and update the second exec call, right now it's using the index for my PC that I know were the OS is installed.
And yes, I know that the user can simply install the OS on a different disk and so the model and/or serial number would be different, but having to go though all that is enough of a hindrance to actually affect my use case, as I only care when the app is launched, and not on how many machines is installed on, that is IF the user finds that I'm using this verification method in the first place.
Any help is appreciated!
|
|
|
|
|
I am not an expert in WMI, but the issue is not that of C++ but your use of the wmic commands. You should first check the actual syntax of the commands you are trying to use. Then run them in a command window to find out the actual correct usage. I tried them both and got the following:
C:\Users\rjmac\source\repos>wmic bootconfig get description
Description
\Device\Harddisk0\Partition1
C:\Users\rjmac\source\repos>wmic diskdrive where DeviceID=\\.\PHYSICALDRIVE0
Node - RJM-INSPIRON15
ERROR:
Description = Invalid query
So I tried this to find out what options are available with the diskdrive sub-command:
C:\Users\rjmac\source\repos>wmic diskdrive -?
DISKDRIVE - Physical disk drive management.
HINT: BNF for Alias usage.
(<alias> [WMIObject] | <alias> [<path where>] | [<alias>] <path where>) [<verb clause>].
USAGE:
DISKDRIVE ASSOC [<format specifier>]
DISKDRIVE CREATE <assign list>
DISKDRIVE DELETE
DISKDRIVE GET [<property list>] [<get switches>]
DISKDRIVE LIST [<list format>] [<list switches>]
The final one allows you to check the options for the command.
So the error message "Description = Invalid query" is telling you that the command is in error, and you need to find out what the correct format is.
|
|
|
|
|
Richard MacCutchan wrote: So the error message "Description = Invalid query" is telling you that the command is in error, and you need to find out what the correct format is.
I did make it work, for some reason I had to use a lot of \ in C++ (I thought you don't need to escape \ in C++, but maybe that is valid in some cases, or maybe I'm just wrong about it).
If used in CMD directly then you need \\\\.\\
I'm still looking for a native use for wmi as that is suggested when used with C++, but at least if I don't find one, you can use this now that is working.
std::cout << exec("wmic diskdrive where DeviceID='\\\\\\\\.\\\\PHYSICALDRIVE3' get model,serialnumber");
//Edit: I'm not looking anymore. I found out that in C++ you need a lot more code to make it work compared with .NET, so I guess you can stick with this, now that it is working.
modified 20-Sep-23 4:59am.
|
|
|
|
|
Yes you always need to escape backslashes in C/C++, as the backslash itself is the escape character. It's also interesting that you need to escape them when using the string in a command window. Glad you found the answer.
|
|
|
|
|
See Valentinor's reply to me below.
|
|
|
|
|