Visual Studio Code with C/C++ extension on Windows 7
Visual Studio Code (VSCode) has become the most popular development environment in the world, and for some good reasons:
But for me the nicest feature, by far, is the remote development. It’s really as advertised and feels like developing on the remote machine itself. No need to transfer the sources back and forth to your local PC anymore, it works like magic. This is very convenient when developing on Linux from a Windows PC, as is the case in many companies.
To achieve this VSCode installs a (node) deamon on the remote machine which interacts with its filesystem and talks to your local VSCode. It’s pretty much VSCode installing itself on the remote server. This applies for the extensions as well. It is a tad overkill, but that is how you can get all the nice features of VSCode even on non local machines.
All of this means these remote binaries must be compatible with the remote host.
But what happens when the remote host runs an old Linux distribution, say, RedHat 6.10?
Nothing. Because it doesn’t work.
But nothing that can’t be overcome.
At my current job, I am given a PC running Windows 7 to do C programming on Linux platform.
That would be fine a situation but:
First issue is easy to work around. VSCode also comes in portable version which can be installed without admin rights on Windows.
But the second and third ones need tricks to overcome. Let’s explain.
To have a functional Visual Studio Code for C/C++ development, we’ll look at the following:
Feel free to skip that section if you are familiar with SSH key authentication.
VSCode doesn’t come with an ssh client. It needs to be installed separately.
Everything is explained on the VSCode Remote Development page. In short:
It doesn’t matter how you install it. But just make sure that you can call sshfrom the command line before going further in this installation guide.
When ssh is properly installed on Windows 7
VSCode allows remote development through SSH and only requires that the authentication is done with authentication keys.
ssh-keygen can be used to generate them on the target Linux (PuTTYgen could also be used if the PuTTY suite is installed on the Windows host). I show the process for RSA keys generated with ssh-keygen here.
> ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/export/home/username/.ssh/id_rsa): id_rsa Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in id_rsa. Your public key has been saved in id_rsa.pub. The key fingerprint is: 11:f9:c3:23:16:fe:08:91:a5:47:d6:07:92:57:fe:72 username@hostname The key's randomart image is: +--[ RSA 2048]----+ | o*+.o. | | o++oo.. | | .oo= .. | | ..+.= . | | oS+ o. E | | . . o | | | | | | | +-----------------+
Two files are output:
The id_rsa private key’s content should look like (I show dummy keys):
-----BEGIN RSA PRIVATE KEY----- MIIEoQIBAAKCAQEA6yVemVZtnm16/Fx7tVbOMl73xGuECnNB6zuLjEawy42yK+0F +UphiWbZk9kwmaQH2si3NEcURpr/DuSooV2Dyy/EhKyCZYtjY+sTZ2d+khR0JICF qv1G49PB7UVuQvdp+FzapUsZtpehLvWPTu2gpqE5c3qJQEHn0XsCHMqi2qB1cDHT V44BUCwszAbsAantkio6th3bEp3XXxxe3Ly7CoiN9qY8zODZs57bw/IQ0z5foULO TLr1F7GAPcWcFHpUC4QjJrLs0yTX/C/ANKi3TNWjVQaJGqJJ9XzwsLtyBzqq0agU T0kpKdWnrrf29hmqThmTxZIGhQxo+k1fYUOjhQIBIwKCAQA8d1LW+PedxGF0F8gK DwHScDEVQDflbhhDzXunumf50/qdlLhkrLn+wqz6Iefs/kspoVOuW2udIIq6r9OX NU3GiKA/X48+rtBqJoFVGpzjvB3dgCJfKy98aablPb1Ea4GmQ8Mx0Xuj+xrRj5nh GIhlXKhfhejHYWd/Al+hAOgMVKdUkirpNKkTwyC2isJ2WZ9UoUl/TiIHhooGXMtu VbuKgCZIuaDSHWjuQYd4kgJSkt3e/yer6Esbn9ezwHZa5AszxGvwiuTBXVVqpsR3 KtiKj1MDds7dUdQAmnHBobwK8H8wKXycAyATqfqGh/qrYMPpUW+YjOPjssuCidBD PBevAoGBAPaS0K2CJfSxVgSRxBLYAi4S5lxSV0jFQpolK21RCEytYlIkicv1eZ+j 13g26iOMsbFBetSdLtv1CgLv4U/bHxl3vYoYG0c4zrXFcsFg3cfblefpPbE93ONz TfvlHlc3pLZ728NKUoMPpeY2lphB8MjopKcu1i5ZPFFzHEu0ii+bAoGBAPQitvC8 RezgNKkCmQFdZ6k2Jfl0sypo+rKva6GJSpUJpabwLhvfLb5xOlS4JXJ3NRanPQoT BLwpAP2S22OCjw8DWPVKtGTjQIC0bVggzvqrvQbQF0Gchy4ODzpo0s3BfOlFKvqm P9QGmrpQglb9PssS/1cMl/AoGMc2NZRuXftfAoGBAKkUN1Jn34M3xfSBNf5K+i44 2HnSD/disVPBt2DnG6JLAZdpgw+DwR0ComEPtn7G4D8IVDoFYfXp2vqzHighOeWT 768meRrlIAedVgDx9yn1qJ8GVjBk7zzhWgvQT1G/wWcwXC4kVdY2naUsvwlRya5W YkbBB+VEgSHoiG5tK45bAoGAG+a10l6o598b9g7s6jaPgQ2ArsuCMLuKXZBkEnYX JvnJye+QPbMbK7UrPOHYZNpsd54VmsBYUASvp/OGyYtDjK/s6NVWce4WABSeyD5D ilzFJVmcQgNCpi1+FU3O8vGKnlEMOecdPM2OBqodhkjT+fOK5WCGZJbeQqccEPar p7MCgYB6l0ztkxbQPPzonFzkQ0tmYOUa64Bysg328/K2gdqcduu9ZKxFiOThAHYx wxHGJmAg4BLefFPkW/7QHSPtNLl5XCnEzx5iCHxPe5cyE/HJozgLmnRjiAdJ4mOX wqtcR3J/cLoAnxAMooLlvoLwR48HtYALKYVnD0YOSIaIInrEgA== -----END RSA PRIVATE KEY-----v
id_rsa.pub’s content looks like:
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6yVemVZtnm16/Fx7tVbOMl73xGuECnNB6zuLjEawy42yK+0F+UphiWbZk9kwmaQH2si3NEcURpr/DuSooV2Dyy/EhKyCZYtjY+sTZ2d+khR0JICFqv1G49PB7UVuQvdp+FzapUsZtpehLvWPTu2gpqE5c3qJQEHn0XsCHMqi2qB1cDHTV44BUCwszAbsAantkio6th3bEp3XXxxe3Ly7CoiN9qY8zODZs57bw/IQ0z5foULOTLr1F7GAPcWcFHpUC4QjJrLs0yTX/C/ANKi3TNWjVQaJGqJJ9XzwsLtyBzqq0agUT0kpKdWnrrf29hmqThmTxZIGhQxo+k1fYUOjhQ== username@hostname
Append the public key to your $HOME/.ssh/authorized_keys file. If the folder and file don’t exist, create them and make sure they have 700 rights for .ssh folder
drwx------ 2 username group 4096 May 22 09:23 .ssh
and 600 rights for authorized_keys
-rw------- 1 username group 391 May 22 09:21 authorized_keys
Retrieve the private key on your local PC and put it somewhere safe. Delete it from the remote (our leave it in .ssh/ with 600 rights).
Start VSCode, and switch to Explorer view (the terminal icon on the left bar). Add an SSH target by clicking the “+” on the SSH TARGETS bar. Then input the following command:
ssh username@hostname
VSCode prompts you to choose a config file and ask to open it. Accept. Since I installed ssh with Git, my ssh config file is:
%GIT_INSTALL_DIR%\etc\ssh\ssh_config
You’ll see that VSCode has already added the new SSH host:
Host xxxxxxxx HostName xxxxxxxx User username
Complete the setup by adding the path to your private key, as shown in bold below:
Host xxxxxxxx HostName xxxxxxxx User username IdentityFile C:\Path\to\your\id_rsa
You can also rename the Host (and not the HostName!) to your liking. I chose dev_server. Restart VSCode if the new name doesn’t update in SSH TARGETS.
Click the add icon on the right. Choose Linux as the target platform and answer “yes” to the fingerprint question.
VSCode tries to start a remote SSH session but miserably fails:
Visual Studio Code cannot start the remote process
What’s happening? VSCode is not very clear about that, so let’s check on the remote server itself. From the remote user home directory, run:
> ls -al | grep vscode drwxr-xr-x 3 username group 4096 May 25 16:08 .vscode-server
VSCode has put something there. That’s the remote daemon we mentioned, which acts as a delegate to manipulate the remote Linux file system and show them as being “local” on VSCode. We are looking for the executables in ELF format (The output is overwhelming so I have stripped the middle part):
> find .vscode-server/ -exec file {} \; | grep -i elf .vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/node: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, not stripped ... ... ... .vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/node_modules/vsda/build/Release/vsda.node: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped
node seems like a good candidate. It’s very likely the daemon our local VSCode talks to. Running it gives:
> cd .vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/ > ./node ./node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.14' not found (required by ./node) ./node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.18' not found (required by ./node) ./node: /usr/lib64/libstdc++.so.6: version `CXXABI_1.3.5' not found (required by ./node) ./node: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by ./node) ./node: /lib64/libc.so.6: version `GLIBC_2.17' not found (required by ./node) ./node: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by ./node) ./node: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by ./node)
No wonder! node doesn’t find the proper version of the C/C++ libs against which it was linked. We can check the C library to confirm:
> strings /lib64/libc.so.6 | grep GLIBC GLIBC_2.2.5 GLIBC_2.2.6 GLIBC_2.3 GLIBC_2.3.2 GLIBC_2.3.3 GLIBC_2.3.4 GLIBC_2.4 GLIBC_2.5 GLIBC_2.6 GLIBC_2.7 GLIBC_2.8 GLIBC_2.9 GLIBC_2.10 GLIBC_2.11 GLIBC_2.12 GLIBC_PRIVATE
The C library’s version is 2.12 and node requires at least 2.14. We’re just shy of 0.2.0.
We need to upgrade the C and C++ standard libs. But without admin rights, we’ll need to cheat a little.
Getting newer version of libraries running on a Linux without administration capability is not difficult but tricky. It is possible to install them from proper RPM packages.
RPM packages can be found on https://pkgs.org/. We want these ones:
For our versions and architecture requirements, these are the packages to use:
We can do the installation in an isolated pseudo “root” folder of the remote session, say, $HOME/local.
~/local > ll total 4036 -rw-r — r — 1 username group 3815032 May 25 18:53 glibc-2.17–307.el7.1.x86_64.rpm -rw-r — r — 1 username group 312504 May 25 18:52 libstdc++-4.8.5–39.el7.x86_64.rpm ~/local >
Obviously, we cannot use rpm nor yum to install these. All we can do is a straightforward extraction. Which is fine, we don’t need the dependencies checks and we — more or less — know what we do.
The actual files of an RPM files lie in a cpio archive inside. The cpio needs to be extracted first before it can be unarchived:
rpm2cpio glibc-2.17–307.el7.1.x86_64.rpm | cpio -idmv rpm2cpio libstdc++-4.8.5–39.el7.x86_64.rpm | cpio -idmv
This creates a folder structure similar to what you’d get under the root folder of a Linux install, but nested under the current directory, here $HOME/local. Once the packages are installed, we can look for the libraries we are trying to upgrade:
~/local > find . -name "lib*.so.6" ./lib64/libc.so.6 ... ./usr/lib64/libstdc++.so.6
Don’t forget to check the libs versions:
~/local > strings ./lib64/libc.so.6 | grep GLIBC ... GLIBC_2.14 GLIBC_2.15 GLIBC_2.16 GLIBC_2.17 GLIBC_PRIVATE ~/local > strings ./usr/lib64/libstdc++.so.6 | grep GLIBCXX ... GLIBCXX_3.4.14 GLIBCXX_3.4.15 GLIBCXX_3.4.16 GLIBCXX_3.4.17 GLIBCXX_3.4.18 GLIBCXX_3.4.19 GLIBCXX_DEBUG_MESSAGE_LENGTH
These are the versions we want.
We need to somehow tell the VSCode binaries to hit these new libs we’ve just installed, instead of the standard ones from the distribution.
A naive thought would be to update $LD_LIBRARY_PATH environment variable to hit our “local” folder before the standard ones but this would break all the other links and render the remote system unusable (don’t try!). Instead we “alter” the links in the binaries themselves. That means:
A utility does just that: patchelf. We’ll also get it from an rpm package: patchelf-0.9–10.el7.x86_64.rpm.
Contrary to the others, we don’t extract it in the local folder, but in a tmpfolder, only to keep the patchelf binary.
~/tmp > rpm2cpio patchelf-0.9–10.el6.x86_64.rpm | cpio -idmv ./usr/bin/patchelf ./usr/share/doc/patchelf-0.9 ./usr/share/doc/patchelf-0.9/COPYING ./usr/share/doc/patchelf-0.9/README ./usr/share/man/man1/patchelf.1.gz 314 blocks ~/tmp > mkdir -p ~/bin ~/tmp > cp usr/bin/patchelf ~/bin ~/tmp > cd .. ~ > rm -fr tmp/
Running it on node:
~/bin/patchelf --set-interpreter $HOME/local/lib64/ld-linux-x86–64.so.2 --set-rpath $HOME/local/usr/lib64/:$HOME/local/lib64 node
(the command was issued from .vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/ folder). There’s no output, so we assume there’s no error. Let’s check:
> ldd node linux-vdso.so.1 => (0x00007ffe1c7da000) libdl.so.2 => /home/username/local/lib64/libdl.so.2 (0x00007fbf1d870000) librt.so.1 => /home/username/local/lib64/librt.so.1 (0x00007fbf1d667000) libstdc++.so.6 => /home/username/local/usr/lib64/libstdc++.so.6 (0x00007fbf1d360000) libm.so.6 => /home/username/local/lib64/libm.so.6 (0x00007fbf1d05e000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003e63a00000) libpthread.so.0 => /home/username/local/lib64/libpthread.so.0 (0x00007fbf1ce37000) libc.so.6 => /home/username/local/lib64/libc.so.6 (0x00007fbf1ca69000) /home/username/local/lib64/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86–64.so.2 (0x00005639414e6000)
It seems to work. We see that we have missed libgcc but the version on our system is good enough.
If we open a session to dev_server again…
There still is a prominent warning, but it is just a warning and is because VSCode detects old versions of the standard C and C++ libs in the library paths. The thing to look at is the bright orange banner at the bottom left showing we are connected!
We can further confirm by checking the processes on the remote host:
> ps -fu username | grep vscode 10929 13188 13162 0 20:43 ? 00:00:00 sh /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/server.sh — host=127.0.0.1 — enable-remote-auto-shutdown — port=0 10929 13196 13188 0 20:43 ? 00:00:01 /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/node /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/out/vs/server/main.js — host=127.0.0.1 — enable-remote-auto-shutdown — port=0 10929 13282 13196 0 20:43 ? 00:00:01 /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/node /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/out/bootstrap-fork — type=extensionHost — uriTransformerPath=/home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/out/vs/server/uriTransformer.js 10929 13303 13282 0 20:43 ? 00:00:00 /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/node /home/username/.vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/extensions/json-language-features/server/dist/jsonServerMain — node-ipc — clientProcessId=13282 10929 13355 2298 0 20:46 pts/5 00:00:00 grep vscode
The Node JS server is running! It shows many processes because nodepreforks them upon launch (so this is as many processes you’ll have).
Back to VSCode, try to open a folder and you’ll be navigating on the remote filesystem. A workspace can be chosen from here.
All the other ELF binaries in .vscode-server/bin/ff915844119ce9485abfe8aa9076ec76b5300ddd/ shall be patched the same way. Use the command to find ELF files shown above, and patch them with patchelf. ELF libraries don’t have an interpreter, so there’s no need to specify a replacement and the command line to patch them is:
~/bin/patchelf — set-rpath $HOME/local/usr/lib64/:$HOME/local/lib64 lib_to_patch
Check the results with ldd after each patchelf.
The C/C++ functionalities of VSCode are found in an extension:
Extensions should be carefully chosen, but that one has millions of downloads, good reviews and is backed by Microsoft, so no worries here. Don’t be fooled by the low version number, it is fully functional.
Notice the button says “Install in SSH: dev_server”. That makes sense given all we have said so far. But that also means we are going to run into similar linking problems…and indeed, after the installation, we get:
Sigh…
So we go to the download page and get the latest cpptools-linux.vsixpackage:
We drop it in the remote $HOME folder, and launch the “Install from VSIX” command from the Command Palette (F1):
The remote $HOME folder shows up and we can see the just added .vsixpackage:
Pick it. The install completes fairly quickly and then prompts to reload VSCode. This only gets us another error message:
Hopefully the last error message…
Again, VSCode is not clear about the problem but it is the same: wrong versions of librairies linked to the extension binaries. Given that extensions install themselve in $HOME/.vscode-server/extensions on the remote Linux, the following identified ELF binaries must be patched:
~/.vscode-server/extensions/ms-vscode.cpptools-0.28.1 > find . -exec file {} \; | grep -i elf ./bin/cpptools-srv: ELF 64-bit LSB executable, x86–64, version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, not stripped ./bin/cpptools: ELF 64-bit LSB executable, x86–64, version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, not stripped ./debugAdapters/mono.linux-x86_64: ELF 64-bit LSB executable, x86–64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, stripped ./LLVM/bin/clang-format: ELF 64-bit LSB executable, x86–64, version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, not stripped
Here’s a cleaned up version of the output:
./bin/cpptools-srv ./bin/cpptools ./debugAdapters/mono.linux-x86_64 ./LLVM/bin/clang-format
These are all executables binaries, so the interpreter must be changed when elfpatching them.
~/bin/patchelf --set-interpreter $HOME/local/lib64/ld-linux-x86-64.so.2 --set-rpath $HOME/local/usr/lib64/:$HOME/local/lib64 file_to_patch
Note that patchelf only process one file at a time so the command must be issued for each of them.
After a reload of the VSCode window (“Reload Window” from the command palette) you’ll have a fully working Visual Studio Code with C/C++ extension enabled!
It’s finally working! Visual Studio Code and the C/C++ extension in action.
We can put the SSH integration to test. Fire up a terminal (“Open New External Terminal” in the Command Palette) and type
> touch hello.txt
The hello.txt file immediately appears in the explorer, at the left of the screen.
Again, it really feels like developing on a local machine.
I find it much convenient to use a graphical debugger over the command line. But can we make it work on our patched up environment?
Use this little C program to check:
Compile it and run it:
> gcc test.c -o test > ./test Message: It works!
Let’s try to run it with the debugger. Switch to the Run view and create a launch.json file.
Choose:
VSCode generates a launch.json file automatically. Change it to make the debugger stop at the beginning (stopAtEntry set to “true”):
{ "version": "0.2.0", "configurations": [ { "name": "gcc - Build and debug active file", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}", "args": [], "stopAtEntry": true, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "gcc build active file", "miDebuggerPath": "/usr/bin/gdb" } ] }
It also generates a tasks.json file, which defines a pre task to run before firing up the debugger. It is typically a compilation.
{ "tasks": [ { "type": "shell", "label": "gcc build active file", "command": "/usr/bin/gcc", "args": [ "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "/usr/bin" } } ], "version": "2.0.0" }
Note that it is clever enough to compile with the -g flag.
Make sure the test.c file is in focus, and launch the just defined “gcc — Build and debug active file” task.
Graphical gdb
It works! You can see it is possible to inspect variables, to step into functions, to look at the call stack, and to add breakpoints… we have a full fledged graphical debugger.
So yes, we can make the graphical debugger work. But while it seemed like a breeze to have it work, my investigations took a bit longer as I didn’t immediately realise that the ./debugAdapters/mono.linux-x86_64 file of the C/C++ extension needed to be elfpatched as well.
Some drawback of that workaround:
But in my view these little shortcomings are easily compensated by how nicer of a development environment this has gotten me.
https://pkgs.org/